diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 0000000000..76ed32d5e2 --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,27 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "altcover.global": { + "version": "9.0.102", + "commands": [ + "altcover" + ], + "rollForward": false + }, + "coveralls.net": { + "version": "4.0.1", + "commands": [ + "csmacnz.Coveralls" + ], + "rollForward": false + }, + "nunit.consolerunner.netcore": { + "version": "3.22.0", + "commands": [ + "nunit" + ], + "rollForward": false + } + } +} \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..7481ade57b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,201 @@ +# Remove the line below if you want to inherit .editorconfig settings from higher directories +root = true + +# C# files +[*.cs] + +#### Core EditorConfig Options #### + +# Indentation and spacing +indent_size = 4 +indent_style = tab +tab_width = 4 + +# New line preferences +end_of_line = crlf +insert_final_newline = true + +#### .NET Coding Conventions #### + +# Organize usings +dotnet_separate_import_directive_groups = true +dotnet_sort_system_directives_first = true +file_header_template = unset + +# this. and Me. preferences +dotnet_style_qualification_for_event = false:silent +dotnet_style_qualification_for_field = false:silent +dotnet_style_qualification_for_method = false:silent +dotnet_style_qualification_for_property = false:silent + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true:silent +dotnet_style_predefined_type_for_member_access = true:silent + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent + +# Expression-level preferences +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_object_initializer = true:suggestion +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion + +# Field preferences +dotnet_style_readonly_field = true:suggestion + +# Parameter preferences +dotnet_code_quality_unused_parameters = all:suggestion + +#### C# Coding Conventions #### + +# var preferences +csharp_style_var_elsewhere = false:silent +csharp_style_var_for_built_in_types = false:silent +csharp_style_var_when_type_is_apparent = false:silent + +# Expression-bodied members +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent + +# Pattern matching preferences +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_prefer_switch_expression = true:suggestion + +# Null-checking preferences +csharp_style_conditional_delegate_call = true:suggestion + +# Modifier preferences +csharp_prefer_static_local_function = true:suggestion +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:silent + +# Code-block preferences +csharp_prefer_braces = true:silent +csharp_prefer_simple_using_statement = true:suggestion + +# Expression-level preferences +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_pattern_local_over_anonymous_function = true:suggestion +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_throw_expression = true:suggestion +csharp_style_unused_value_assignment_preference = discard_variable:suggestion +csharp_style_unused_value_expression_statement_preference = discard_variable:silent + +# 'using' directive preferences +csharp_using_directive_placement = outside_namespace:silent + +#### C# Formatting Rules #### + +# New line preferences +csharp_new_line_before_catch = false +csharp_new_line_before_else = false +csharp_new_line_before_finally = false +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = methods,types +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = false + +# Space preferences +csharp_space_after_cast = true +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = true +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = true +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Wrapping preferences +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000000..c1e6522536 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: jstedfast diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000000..3ab689b096 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,61 @@ +--- +name: Bug report +about: Create a report to help us improve + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**Platform (please complete the following information):** + - OS: [e.g. Windows, Linux, MacOS, iOS, Android, Windows Phone, etc.] + - .NET Runtime: [e.g. CoreCLR, Mono] + - .NET Framework: [e.g. .Net Core, .NET 4.5, UWP, etc.] + - MailKit Version: + +**Exception** +If you got an exception, please include the exception Message *and* StackTrace. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Code Snippets** +If applicable, add code snippets to help explain your problem. + +```csharp +// Add your code snippet here. +``` + +**Protocol Logs** +Please include a protocol log (scrubbed of any authentication data), especially +if you got an exception such as `Syntax error in XYZ. Unexpected token: ...`. + +To get a protocol log, follow one of the following code snippets: + +```csharp +// log to a file called 'imap.log' +var client = new ImapClient (new ProtocolLogger ("imap.log")); +``` + +```csharp +// log to a file called 'pop3.log' +var client = new Pop3Client (new ProtocolLogger ("pop3.log")); +``` + +```csharp +// log to a file called 'smtp.log' +var client = new SmtpClient (new ProtocolLogger ("smtp.log")); +``` + +Note: if the protocol log contains sensitive information, feel free to email it to me at +jestedfa@microsoft.com instead of including it in the GitHub issue. + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000000..066b2d920a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,17 @@ +--- +name: Feature request +about: Suggest an idea for this project + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..2916857246 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + - package-ecosystem: "nuget" + directory: "/MailKit" + schedule: + interval: "weekly" + day: "monday" + time: "08:00" + timezone: "America/New_York" diff --git a/.github/issue_template.md b/.github/issue_template.md deleted file mode 100644 index 1ee38ad36a..0000000000 --- a/.github/issue_template.md +++ /dev/null @@ -1,31 +0,0 @@ -To help me debug your issue, please explain: -- What were you trying to do? -- What happened? -- What did you expect to happen? -- Step-by-step reproduction instructions and/or a simple test case. - -If you got an exception, please include the exception Message *and* StackTrace. - -If you got an exception such as `Syntax error in XYZ. Unexpected token: ...`, -INCLUDE THE PROTOCOL LOG (scrubbed of any authentication data). If you do not include -the protocol log, you will make me VERY UNHAPPY. - -To get a protocol log, follow one of the following code snippets: - -```csharp -// log to a file called 'imap.log' -var client = new ImapClient (new ProtocolLogger ("imap.log")); -``` - -```csharp -// log to a file called 'pop3.log' -var client = new Pop3Client (new ProtocolLogger ("pop3.log")); -``` - -```csharp -// log to a file called 'smtp.log' -var client = new SmtpClient (new ProtocolLogger ("smtp.log")); -``` - -Note: if the protocol log contains sensitive information, feel free to email it to me at -jestedfa@microsoft.com instead of including it in the GitHub issue. diff --git a/.github/workflows/aot-compatibility.yml b/.github/workflows/aot-compatibility.yml new file mode 100644 index 0000000000..b77574f269 --- /dev/null +++ b/.github/workflows/aot-compatibility.yml @@ -0,0 +1,38 @@ +name: AOT Compatibility + +on: + push: + branches: [ 'master' ] + paths-ignore: + - '**.md' + pull_request: + branches: [ 'master' ] + paths-ignore: + - '**.md' + +jobs: + aot-test: + strategy: + fail-fast: false # ensures the entire test matrix is run, even if one permutation fails + matrix: + os: [ windows-latest ] + mailkitlite: [ true ] + + runs-on: ${{ matrix.os }} + steps: + - name: Setup/Install the .NET SDKs + uses: actions/setup-dotnet@v5 + with: + dotnet-version: | + 8.0.x + 10.0.x + + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 # fetching all + submodules: true + + - name: Publish AOT testApp, assert static analysis warning count, and run the app + shell: pwsh + run: .\scripts\test-aot-compatibility.ps1 ${{ matrix.mailkitlite }} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000000..2802c824ea --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,67 @@ +name: "CodeQL" + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + schedule: + - cron: '38 12 * * 1' + +jobs: + analyze: + name: Analyze + runs-on: 'ubuntu-latest' + timeout-minutes: 360 + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'csharp' ] + + steps: + - name: Setup/Install the .NET SDKs + uses: actions/setup-dotnet@v5 + with: + dotnet-version: | + 8.0.x + 10.0.x + + - name: Checkout repository + uses: actions/checkout@v6 + with: + token: ${{ secrets.GITHUB_TOKEN }} + submodules: recursive + fetch-depth: 0 + + - name: Run .NET restore + shell: pwsh + run: | + dotnet restore MailKit.sln + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + queries: security-extended,security-and-quality + + - name: Build + continue-on-error: false + shell: pwsh + run: | + dotnet msbuild MailKit.sln -property:Platform="Any CPU" -property:Configuration=Release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000000..cf27696394 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,155 @@ +name: Build + +on: [push, pull_request, workflow_dispatch] + +jobs: + ci: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ windows-latest, ubuntu-latest ] + build-configuration: [ Debug, Release ] + outputs: + latest-version: ${{ steps.semantic_version.outputs.version_num }} + environment: ci + env: + SOLUTION_PATH: MailKit.sln + BUILD_PLATFORM: Any CPU + BUILD_CONFIGURATION: ${{ matrix.build-configuration }} + GENERATE_CODE_COVERAGE: ${{ matrix.os == 'windows-latest' && matrix.build-configuration == 'Debug' }} + MONO_RUNTIME: ${{ matrix.os != 'windows-latest' }} + PUBLISH: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && matrix.os == 'windows-latest' && matrix.build-configuration == 'Release' }} + + steps: + - name: Setup/Install the .NET SDKs + id: install-netsdk + uses: actions/setup-dotnet@v5 + with: + dotnet-version: | + 8.0.x + 10.0.x + + - if: runner.os == 'Windows' + name: Setup MSBuild + id: setup_msbuild + uses: microsoft/setup-msbuild@v3 + + - name: Checkout repository + id: checkout_repo + uses: actions/checkout@v6 + with: + token: ${{ secrets.GITHUB_TOKEN }} + submodules: recursive + fetch-depth: 0 + + - name: Get semantic version from csproj + id: semantic_version + shell: pwsh + run: | + $xml = [xml](gc MailKit/MailKit.csproj) + $SEMANTIC_VERSION_NUMBER = $xml.Project.PropertyGroup.VersionPrefix + $VERSION_NUM = $SEMANTIC_VERSION_NUMBER[0].Trim() + Write-Host "version_num=${VERSION_NUM}" + [IO.File]::AppendAllText($env:GITHUB_OUTPUT, "version_num=${VERSION_NUM}$([Environment]::NewLine)") + + - if: ${{ env.PUBLISH == 'true' }} + name: Get latest tag + id: get_latest_tag + shell: pwsh + run: | + $LATEST_TAG = git -c 'versionsort.suffix=-' ls-remote --exit-code --refs --sort='version:refname' --tags "https://github.com/$env:GIT_URL.git" '*.*.*' | tail --lines=1 | cut --delimiter='/' --fields=3 + Write-Host "tag=$LATEST_TAG" + [IO.File]::AppendAllText($env:GITHUB_OUTPUT, "tag=${LATEST_TAG}$([Environment]::NewLine)") + env: + GIT_URL: ${{ github.repository }} + + - if: ${{ env.PUBLISH == 'true' && steps.semantic_version.outputs.version_num != steps.get_latest_tag.outputs.tag }} + name: Add new tag to repo + id: add_new_tag_to_repo + continue-on-error: true + shell: pwsh + run: | + git config --global user.name $env:GIT_USER_NAME + git config --global user.email $env:GIT_USER_EMAIL + git tag -a -m "Tagged for $env:NEW_VERSION_NUM" $env:NEW_VERSION_NUM + git push --follow-tags + env: + GIT_USER_NAME: ${{ github.event.head_commit.author.username }} + GIT_USER_EMAIL: ${{ github.event.head_commit.author.email }} + NEW_VERSION_NUM: ${{ steps.semantic_version.outputs.version_num }} + + - name: Run .NET restore + shell: pwsh + run: | + dotnet restore $env:SOLUTION_PATH + + - name: Run .NET tool restore + shell: pwsh + run: | + dotnet tool restore + + - name: Build solution + id: build_solution + continue-on-error: true + shell: pwsh + run: | + dotnet msbuild $env:SOLUTION_PATH -property:Platform=$env:BUILD_PLATFORM -property:Configuration=$env:BUILD_CONFIGURATION -property:MonoRuntime=$env:MONO_RUNTIME + + - name: Run unit tests + id: run_unit_tests + continue-on-error: true + shell: pwsh + run: | + & ./scripts/test.ps1 -Configuration:$env:BUILD_CONFIGURATION -GenerateCodeCoverage:$env:GENERATE_CODE_COVERAGE + + - name: Upload unit test results + id: upload_test_results + continue-on-error: true + uses: actions/upload-artifact@v7 + with: + name: MailKit.${{ steps.semantic_version.outputs.version_num }}.${{ github.run_number }}-${{ matrix.os }}-${{ matrix.build-configuration }}-TestResults.xml + path: TestResult.xml + + - if: ${{ env.GENERATE_CODE_COVERAGE == 'true' }} + name: Upload code coverage data to coveralls.io + id: upload_to_coveralls + shell: pwsh + run: | + & ./scripts/coveralls.ps1 + env: + COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} + GIT_COMMIT_SHA: ${{ github.sha }} + GIT_REF: ${{ github.ref }} + GIT_ACTOR: ${{ github.event.head_commit.author.username }} + GIT_ACTOR_EMAIL: ${{ github.event.head_commit.author.email }} + GIT_COMMIT_MESSAGE: ${{ github.event.head_commit.message }} + COVERALLS_JOB_ID: ${{ steps.semantic_version.outputs.version_num }}.${{ github.run_number }} + + - if: ${{ env.PUBLISH == 'true' }} + name: Create NuGet package + id: create_nuget_package + shell: pwsh + run: | + nuget pack nuget/MailKit.nuspec -Version "$env:LATEST_VERSION.$env:GITHUB_RUN_NUMBER" + env: + LATEST_VERSION: ${{ steps.semantic_version.outputs.version_num }} + + - if: ${{ env.PUBLISH == 'true' }} + name: Push NuGet package to MyGet + id: push_nuget_package + shell: pwsh + run: | + nuget push $env:NUGET_PKG_PATH -ApiKey $env:MYGET_API_KEY -Source https://www.myget.org/F/mimekit/api/v3/index.json + env: + NUGET_PKG_PATH: MailKit.${{ steps.semantic_version.outputs.version_num }}.${{ github.run_number }}.nupkg + MYGET_API_KEY: ${{ secrets.MYGET_API_KEY }} + + - if: ${{ env.PUBLISH == 'true' }} + name: Upload NuGet package as artifact + id: upload_nuget_package + uses: actions/upload-artifact@v7 + with: + name: MailKit.${{ steps.semantic_version.outputs.version_num }}.${{ github.run_number }}.nupkg + path: MailKit.${{ steps.semantic_version.outputs.version_num }}.${{ github.run_number }}.nupkg + +# Built with ❤ by [Pipeline Foundation](https://pipeline.foundation) diff --git a/.gitignore b/.gitignore index 0ec45ca90e..31d0a2de5e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +MailKit/Resources/Resource.designer.cs +*.csproj.nuget.dgspec.json +*.csproj.nuget.cache *project.lock.json *.userprefs *.user @@ -8,5 +11,8 @@ packages obj bin .vs +*.patch *.tree *.zip +.idea +.DS_Store diff --git a/.nuget/packages.config b/.nuget/packages.config deleted file mode 100644 index eb80f3b1ce..0000000000 --- a/.nuget/packages.config +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index e8ea6f7d2a..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,9 +0,0 @@ -language: csharp -solution: MailKit.Net45.sln -install: - - git submodule update --init --recursive - - nuget restore submodules/MimeKit/MimeKit.Net45.sln - - nuget restore MailKit.Net45.sln -script: - - xbuild /p:Configuration=Debug MailKit.Net45.sln - - mono ./packages/NUnit.Runners.2.6.4/tools/nunit-console.exe UnitTests/bin/Debug/UnitTests.dll diff --git a/AotCompatibility/AotCompatibility.csproj b/AotCompatibility/AotCompatibility.csproj new file mode 100644 index 0000000000..77cd079eb3 --- /dev/null +++ b/AotCompatibility/AotCompatibility.csproj @@ -0,0 +1,31 @@ + + + + Exe + net8.0 + enable + enable + true + false + true + true + false + + + + + + + + + + + + + + + + + + + diff --git a/AotCompatibility/Program.cs b/AotCompatibility/Program.cs new file mode 100644 index 0000000000..0a4a96921d --- /dev/null +++ b/AotCompatibility/Program.cs @@ -0,0 +1,57 @@ +// +// Program.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2024 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System.Text; + +using MimeKit; +using MailKit; +using MailKit.Net; +using MailKit.Net.Imap; +using MailKit.Net.Pop3; +using MailKit.Net.Smtp; + +namespace AotCompatibility { + class Program + { + static int Main (string[] args) + { + try { + Encoding.RegisterProvider (CodePagesEncodingProvider.Instance); + + var message = new MimeMessage (); + + using (var imap = new ImapClient ()) {} + using (var pop3 = new Pop3Client ()) {} + using (var smtp = new SmtpClient ()) {} + + return 0; + } catch (Exception ex) { + Console.WriteLine (ex); + return -1; + } + } + } +} \ No newline at end of file diff --git a/Documentation/Content/Frequently-Asked-Questions.aml b/Documentation/Content/Frequently-Asked-Questions.aml index f259e86111..3efe9a4f39 100644 --- a/Documentation/Content/Frequently-Asked-Questions.aml +++ b/Documentation/Content/Frequently-Asked-Questions.aml @@ -1,4 +1,4 @@ - + - -
- Why do I get 'The remote certificate is invalid according to the validation procedure' when I try to Connect? + +
+ Why do I get "NotSupportedException: No data is available for encoding ######."? - When you get an exception with that error message, it means that the IMAP, POP3 or SMTP - server that you are connecting to is using an SSL certificate that is either expired - or untrusted by your system. - + In .NET Core, Microsoft decided to split out the non-Unicode text encodings into a separate NuGet package called + + System.Text.Encoding.CodePages + https://www.nuget.org/packages/System.Text.Encoding.CodePages + _blank + . + + + MimeKit already pulls in a reference to this NuGet package, so you shouldn't need to add a reference to it in + your project. That said, you will still need to register the encoding provider. It is recommended that you add + the following line of code to your program initialization (e.g. the beginning of your program's Main() method): + + + System.Text.Encoding.RegisterProvider (System.Text.CodePagesEncodingProvider.Instance); + + +
+ +
+ Why does text show up garbled in my ASP.NET Core / .NET Core / .NET 5+ app? + - Often times, mail servers will use self-signed certificates instead of using a certificate - that has been signed by a trusted Certificate Authority. When your system is unable to - validate the mail server's certificate because it is not signed by a known and trusted - Certificate Authority, the above error will occur. + .NET Core (and ASP.NET Core by extension) and .NET 5 (and later) only provide the Unicode encodings, ASCII and ISO-8859-1 by default. + Other text encodings are not available to your application unless your application + + registers + https://docs.microsoft.com/en-us/dotnet/api/system.text.encoding.registerprovider?view=net-5.0 + _blank + the encoding + provider that provides all of the additional encodings. - You can work around this problem by supplying a custom - RemoteServerCertificateValidationCallback - https://msdn.microsoft.com/en-us/library/ms145054 + First, add a package reference for the + + System.Text.Encoding.CodePages + https://www.nuget.org/packages/System.Text.Encoding.CodePages _blank - - and setting it on the client's P:MailKit.MailService.ServerCertificateValidationCallback property. + nuget package to your project and then register the additional text encodings using the following code snippet: + + System.Text.Encoding.RegisterProvider (System.Text.CodePagesEncodingProvider.Instance); + - In the most simplest example, you could do something like this (although I would strongly recommend against it in production use): + Note: The above code snippet should be safe to call in .NET Framework versions >= 4.6 as well. - + +
+ +
+ Why do I get a TypeLoadException when I try to create a new MimeMessage? + - Most likely you'll want to instead compare the certificate's - Thumbprint - https://msdn.microsoft.com/en-us/library/system.security.cryptography.x509certificates.x509certificate2.thumbprint(v=vs.110).aspx - _blank - - property to a known value that you have verified at a prior date. + This only seems to happen in cases where the application is built for .NET Framework (v4.x) and seems to be most + common for ASP.NET web applications that were built using Visual Studio 2019 (it is unclear whether this happens + with Visual Studio 2022 as well). + + + The issue is that some (older?) versions of MSBuild do not correctly generate *.dll.config, + app.config and/or web.config files with proper assembly version binding + redirects. + + + If this problem is happening to you, make sure to use MimeKit and MailKit >= v4.0 which include MimeKit.dll.config + and MailKit.dll.config. + + + The next step is to manually edit your application's app.config (or web.config) to add a + binding redirect for System.Runtime.CompilerServices.Unsafe: + + + <configuration> + <runtime> + <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> + <dependentAssembly> + <assemblyIdentity name="System.Runtime.CompilerServices.Unsafe" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/> + <bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0" /> + </dependentAssembly> + </assemblyBinding> + </runtime> + </configuration> + + +
+ +
+ Why do I get "An error occurred while attempting to establish an SSL or TLS connection." when I try to Connect? + + + When you get an exception with that error message, it usually means that you are encountering + one of the following scenarios: + + + The mail server does not support SSL on the specified port. + + There are 2 different ways to use SSL/TLS encryption with mail servers. + + + The first way is to enable SSL/TLS encryption immediately upon connecting to the + SMTP, POP3 or IMAP server. This method requires an "SSL port" because the standard + port defined for the protocol is meant for plain-text communication. + + + The second way is via a STARTTLS command (aka STLS for POP3) that is + optionally supported by the server. + + + Below is a table of the protocols supported by MailKit and the standard plain-text ports + (which either do not support any SSL/TLS encryption at all or only via the STARTTLS + command extension) and the SSL ports which require SSL/TLS encryption immediately upon a + successful connection to the remote host. + + + + + + Protocol + + + Standard Port + + + SSL Port + + + + + + SMTP + + + 25 or 587 + + + 465 + + + + + POP3 + + + 110 + + + 995 + + + + + IMAP + + + 143 + + + 993 + + +
+ + It is important to use the correct T:MailKit.Security.SecureSocketOptions for + the port that you are connecting to. + + + If you are connecting to one of the standard ports above, you will need to use SecureSocketOptions.None, + SecureSocketOptions.StartTls or SecureSocketOptions.StartTlsWhenAvailable. + + + If you are connecting to one of the SSL ports, you will need to use SecureSocketOptions.SslOnConnect. + + + You could also try using SecureSocketOptions.Auto which works by choosing the appropriate option to use + by comparing the specified port to the ports in the above table. + +
+ + The mail server that you are connecting to is using an expired (or otherwise untrusted) SSL certificate. + + Often times, mail servers will use self-signed certificates instead of using a certificate that + has been signed by a trusted Certificate Authority. Another potential pitfall is when locally + installed anti-virus software replaces the certificate in order to scan web traffic for viruses. + + + When your system is unable to validate the mail server's certificate because it is not signed + by a known and trusted Certificate Authority, the above error will occur. + + + You can work around this problem by supplying a custom + + RemoteServerCertificateValidationCallback + https://msdn.microsoft.com/en-us/library/ms145054 + _blank + + and setting it on the client's P:MailKit.MailService.ServerCertificateValidationCallback + property. + + + In the simplest example, you could do something like this (although I would strongly recommend against it in + production use): + + + using (var client = new SmtpClient ()) { + client.ServerCertificateValidationCallback = (s,c,h,e) => true; + + client.Connect (hostName, port, SecureSocketOptions.Auto); + + // ... + } + + + A better solution might be to compare the certificate's common name, issuer, serial number, and fingerprint + to known values to make sure that the certificate can be trusted. Take the following code snippet as an + example of how to do this: + + + bool MyServerCertificateValidationCallback (object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) + { + if (sslPolicyErrors == SslPolicyErrors.None) + return true; + + // Note: The following code casts to an X509Certificate2 because it's easier to get the + // values for comparison, but it's possible to get them from an X509Certificate as well. + if (certificate is X509Certificate2 certificate2) { + var cn = certificate2.GetNameInfo (X509NameType.SimpleName, false); + var fingerprint = certificate2.Thumbprint; + var serial = certificate2.SerialNumber; + var issuer = certificate2.Issuer; + + return cn == "imap.gmail.com" && issuer == "CN=GTS CA 1O1, O=Google Trust Services, C=US" && + serial == "00BABE95B167C9ECAF08000000006065B6" && + fingerprint == "E79A011EF55EEC72D2B7E391D193761372796836"; + } + + return false; + } + + + The downside of the above example is that it requires hard-coding known values for "trusted" mail server + certificates which can quickly become unwieldy to deal with if your program is meant to be used with + a wide range of mail servers. + + + The best approach would be to prompt the user with a dialog explaining that the certificate is + not trusted for the reasons enumerated by the + + SslPolicyErrors + https://docs.microsoft.com/en-us/dotnet/api/system.net.security.sslpolicyerrors?view=netframework-4.8 + _blank + + argument as well as potentially the errors provided in the + + X509Chain + https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.x509certificates.x509chain?view=netframework-4.8 + _blank + + If the user wishes to accept the risks of trusting the certificate, your program could then return true. + + + For more details on writing a custom SSL certificate validation callback, it may be worth checking out the + + SslCertificateValidation.cs + https://github.com/jstedfast/MailKit/blob/master/Documentation/Examples/SslCertificateValidation.cs + _blank + + example. + + + + A Certificate Authority CRL server for one or more of the certificates in the chain is temporarily unavailable. + + Most Certificate Authorities are probably pretty good at keeping their CRL and/or OCSP servers up 24/7, but occasionally + they do go down or are otherwise unreachable due to other network problems between you and the + server. When this happens, it becomes impossible to check the revocation status of one or more of the certificates in the chain. + + + To ignore revocation checks, you can set the + P:MailKit.MailService.CheckCertificateRevocation + property of the IMAP, POP3 or SMTP client to false before you connect: + + + using (var client = new SmtpClient ()) { + client.CheckCertificateRevocation = false; + + client.Connect (hostName, port, SecureSocketOptions.Auto); + + // ... + } + + + + The server does not support the same set of SSL/TLS protocols that the client is configured to use. + + MailKit attempts to keep up with the latest security recommendations and so is continuously removing older SSL and TLS + protocols that are no longer considered secure from the default configuration. This often means that MailKit's SMTP, + POP3 and IMAP clients will fail to connect to servers that are still using older SSL and TLS protocols. Currently, + the SSL and TLS protocols that are not supported by default are: SSL v2.0, SSL v3.0, TLS v1.0 and TLS v1.1. + + + You can override MailKit's default set of supported + + SSL and TLS protocols + https://docs.microsoft.com/en-us/dotnet/api/system.security.authentication.sslprotocols?view=netframework-4.8 + _blank + + by setting the value of the P:MailKit.MailService.SslProtocols + property on your SMTP, POP3 or IMAP client. + + For example: + + using (var client = new SmtpClient ()) { + // Allow SSLv3.0 and all versions of TLS + client.SslProtocols = SslProtocols.Ssl3 | SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12 | SslProtocols.Tls13; + + client.Connect ("smtp.gmail.com", 465, true); + + // ... + } + + +
@@ -94,11 +385,12 @@
- Why don't I see some of my messages when accessing GMail with POP3? + Why doesn't MailKit find some of my GMail POP3 or IMAP messages? - By default, GMail's POP3 server does not behave like a standard POP3 server and hides messages - from clients (as well as having other non-standard behavior) that have already been viewed. + By default, GMail's POP3 and IMAP server does not behave like standard POP3 or IMAP servers + and hides messages from clients using those protocols (as well as having other non-standard + behavior). If you want to configure your GMail POP3 settings to behave the way POP3 is intended to behave, @@ -107,7 +399,7 @@ GMail Settings page and make the following changes in the POP3 Download section: - + Enable POP for all mail (even if it has already been downloaded). @@ -126,12 +418,42 @@ How do I access GMail using MailKit? - The first thing that you will need to do is to configure your GMail account to + As of September 30th, 2024, authentication using only a username and password is + + no longer supported by Google + https://support.google.com/accounts/answer/6010255?hl=en + _blank + . + + + There are now only 2 options to choose from: + + + + + Use OAuth 2.0 to authenticate with GMail. This is the recommended approach. + + + + + Use an App Password to authenticate with GMail. This is a less secure option and is not recommended. + It is only available for accounts that have 2-Step Verification enabled. + + + + + To use an App password, you will first need to + + turn on 2-Step Verification + https://support.google.com/accounts/answer/185839 + _blank + . + Once 2-Step Verification is turned on, you can - enable less secure apps - https://www.google.com/settings/security/lesssecureapps + generate an App password + https://myaccount.google.com/apppasswords _blank - , or you'll need to use OAuth 2.0 authentication (which is a bit more complex). + . Then, assuming that your GMail account is user@gmail.com, you would use the following @@ -140,11 +462,7 @@ using (var client = new ImapClient ()) { client.Connect ("imap.gmail.com", 993, SecureSocketOptions.SslOnConnect); - - // disable OAuth2 authentication unless you are actually using an access_token - client.AuthenticationMechanisms.Remove ("XOAUTH2"); - - client.Authenticate ("user@gmail.com", "password"); + client.Authenticate ("user@gmail.com", "app-specific-password"); // do stuff... @@ -153,33 +471,68 @@ using (var client = new ImapClient ()) { Connecting via POP3 or SMTP is identical except for the host names and ports (and, of course, you'd - use a Pop3Client or SmtpClient as appropriate). + use a Pop3Client or SmtpClient as appropriate). + + +
+ +
+ How can I log in to a GMail account using OAuth 2.0? + + + The first thing you need to do is follow + + Google's instructions + https://developers.google.com/accounts/docs/OAuth2 + _blank + + for obtaining OAuth 2.0 credentials for your application. - If you decide to authenticate via OAuth 2.0 instead of enabling less secure apps, you'll need to read + Or, as an alternative set of step-by-step instructions, you can follow the directions that - Google's OAuth 2.0 documentation - https://developers.google.com/identity/protocols/OAuth2 + I have written + https://github.com/jstedfast/MailKit/blob/master/GMailOAuth2.md _blank - to learn how to do this. + (complete with screenshots). - Once you've got the access_token, you simply use it as if it were the password when - calling the Authenticate method. + Once you've done that, the easiest way to obtain an access token is to use Google's + + Google.Apis.Auth + https://www.nuget.org/packages/Google.Apis.Auth/ + _blank + library: -var certificate = new X509Certificate2 (@"C:\path\to\certificate.p12", "password", X509KeyStorageFlags.Exportable); -var credential = new ServiceAccountCredential (new ServiceAccountCredential.Initializer ("your-developer-id@developer.gserviceaccount.com") { - // Note: other scopes can be found here: https://developers.google.com/gmail/api/auth/scopes - Scopes = new[] { "https://mail.google.com/" }, - User = "user@gmail.com" -}.FromCertificate (certificate)); - -// Note: result will be true if the access token was received successfully -bool result = await credential.RequestAccessTokenAsync (cancel.Token); - -// use the access token as the password string -client.Authenticate ("user@gmail.com", credential.Token.AccessToken); +const string GMailAccount = "username@gmail.com"; + +var clientSecrets = new ClientSecrets { + ClientId = "XXX.apps.googleusercontent.com", + ClientSecret = "XXX" +}; + +var codeFlow = new GoogleAuthorizationCodeFlow (new GoogleAuthorizationCodeFlow.Initializer { + // Cache tokens in ~/.local/share/google-filedatastore/CredentialCacheFolder on Linux/Mac + DataStore = new FileDataStore ("CredentialCacheFolder", false), + Scopes = new [] { "https://mail.google.com/" }, + ClientSecrets = clientSecrets +}); + +var codeReceiver = new LocalServerCodeReceiver (); +var authCode = new AuthorizationCodeInstalledApp (codeFlow, codeReceiver); +var credential = await authCode.AuthorizeAsync (GMailAccount, CancellationToken.None); + +if (authCode.ShouldRequestAuthorizationCode (credential.Token)) + await credential.RefreshTokenAsync (CancellationToken.None); + +var oauth2 = new SaslMechanismOAuth2 (credential.UserId, credential.Token.AccessToken); + +using (var client = new ImapClient ()) { + await client.ConnectAsync ("imap.gmail.com", 993, SecureSocketOptions.SslOnConnect); + await client.AuthenticateAsync (oauth2); + await client.DisconnectAsync (true); +}
@@ -196,7 +549,7 @@ client.Authenticate ("user@gmail.com", credential.Token.AccessToken); You'll probably also want to set the filename parameter on the Content-Disposition header as well as the name parameter on the Content-Type header. The most convenient way to do this - is to simply use the + is to use the P:MimeKit.MimePart.FileName property which will set both parameters for you as well as setting the Content-Disposition header value to attachment if it has not already been set to something else. @@ -208,7 +561,7 @@ client.Authenticate ("user@gmail.com", credential.Token.AccessToken); - For more information, see Creating Messages. + For more information, see Creating Messages. @@ -267,8 +620,8 @@ multipart/mixed The same as above, but with the first part replaced with either - #2 or - #3 + #2 or + #3 To illustrate:
 multipart/mixed
   multipart/alternative
@@ -298,7 +651,7 @@ multipart/mixed
           P:MimeKit.MimeMessage.HtmlBody.
         
         
-          For more information, see .
+          For more information, see .
         
       
     
@@ -494,7 +847,7 @@ message.WriteTo (format, "message.eml");
         
         
           If you only care about getting a flattened list of the mailbox addresses in one of
-          the address headers, you can simply do something like this:
+          the address headers, you can do something like this:
         
         
 foreach (var mailbox in message.To.Mailboxes)
@@ -504,7 +857,7 @@ foreach (var mailbox in message.To.Mailboxes)
     
     
     
- Why do attachments with unicode filenames appear as "ATT0####.dat" in Outlook? + Why do attachments with Unicode filenames appear as "ATT0####.dat" in Outlook? An attachment filename is stored as a MIME parameter on the Content-Disposition @@ -581,7 +934,7 @@ message.WriteTo (options, stream); T:MimeKit.Cryptography.OpenPgpContext includes a - Overload:MimeKit.Cryptography.OpenPgpContext.GetDecryptedStream + M:MimeKit.Cryptography.OpenPgpContext.DecryptTo(System.IO.Stream,System.IO.Stream,System.Threading.CancellationToken) method which can be used to get the raw decrypted stream. @@ -589,7 +942,7 @@ message.WriteTo (options, stream); The method variant that has a T:MimeKit.Cryptography.DigitalSignatureCollection output parameter is useful in cases where the encrypted PGP blurb is also digitally signed, - allowing you to get your hands on the list of digitial signatures in order for you to verify + allowing you to get your hands on the list of digital signatures in order for you to verify each of them. @@ -667,7 +1020,7 @@ message.WriteTo (options, stream); - To forward a message by simply inlining the original message's text content, you can do something like this: + To forward a message by inlining the original message's text content, you can do something like this: diff --git a/Documentation/Content/Getting-Started.aml b/Documentation/Content/Getting-Started.aml index 3e0c54e1fa..b3fe5176e7 100644 --- a/Documentation/Content/Getting-Started.aml +++ b/Documentation/Content/Getting-Started.aml @@ -1,4 +1,4 @@ - + Package Manager Console http://docs.nuget.org/docs/start-here/using-the-package-manager-console - , simply enter the following command: + , enter the following command: Install-Package MailKit @@ -59,39 +59,23 @@ - MailKit.sln includes projects for .NET 4.0, - .NET 4.5, Xamarin.Android, Xamarin.iOS, and the unit tests. + MailKit.sln includes projects for .NET 4.5.2, .NET 4.6, .NET 4.7, + .NET 4.8, .NET 5.0, .NET 6.0, .NETStandard 2.0, .NETStandard 2.1, and the unit tests. - MailKit.Mobile.sln just includes the - Xamarin.Android and Xamarin.iOS projects. - - - - - MailKit.Net45.sln includes the .NET 4.5 - project as well as the unit tests. - - - - - MailKit.Net40.sln just includes the - .NET 4.0 project. + MailKit.Documentation.sln includes projects for generating + the documentation that you are reading right now. - - If you don't have the Xamarin products, you'll probably want to open the - MailKit.Net45.sln instead of - MailKit.sln. - Once you've opened the appropriate MailKit solution file in either Xamarin Studio or - Visual Studio (either will work), you can simply - choose the Debug or Release build configuration and then build. + Visual Studio (either will work), you can + choose the Debug or Release + build configuration and then build.
diff --git a/Documentation/Content/Introduction.aml b/Documentation/Content/Introduction.aml index 9963113dd9..97350de02e 100644 --- a/Documentation/Content/Introduction.aml +++ b/Documentation/Content/Introduction.aml @@ -16,7 +16,7 @@ SASL Authentication - Supports the CRAM-MD5, DIGEST-MD5, LOGIN, NTLM, PLAIN, SCRAM-SHA-1, SCRAM-SHA-256, and XOAUTH2 mechanisms. + Supports the ANONYMOUS, CRAM-MD5, DIGEST-MD5, LOGIN, NTLM, OAUTHBEARER, PLAIN, SCRAM-SHA-1(-PLUS), SCRAM-SHA-256(-PLUS), SCRAM-SHA-512(-PLUS), and XOAUTH2 mechanisms. @@ -29,13 +29,16 @@ Supports DKIM-Signatures. + + Supports ARC signatures. + SMTP Client - Supports SSL and TLS + Supports SSL and TLS. Supports the STARTTLS, SIZE, DSN, 8BITMIME, PIPELINING, BINARYMIME, and SMTPUTF8 extensions. @@ -46,7 +49,7 @@ POP3 Client - Supports SSL and TLS + Supports SSL and TLS. Supports the STLS, UIDL, PIPELINING, UTF8, and LANG extensions. @@ -57,14 +60,25 @@ IMAP Client - Supports SSL and TLS + Supports SSL and TLS. Supports the ACL, QUOTA, LITERAL+, IDLE, NAMESPACE, ID, CHILDREN, LOGINDISABLED, STARTTLS, MULTIAPPEND, UNSELECT, UIDPLUS, CONDSTORE, ESEARCH, SASL-ID, COMPRESS, WITHIN, ENABLE, QRESYNC, - SORT, THREAD, ESORT, METADATA, FILTERS, LIST-STATUS, SORT=DISPLAY, SPECIAL-USE, CREATE-SPECIAL-USE, - SEARCH=FUZZY, MOVE, UTF8=ACCEPT, UTF8=ONLY, LITERAL-, APPENDLIMIT, XLIST, - and the Google Mail extensions. + SORT, THREAD, ANNOTATE, LIST-EXTENDED, ESORT, METADATA, METADATA-SERVER, NOTIFY, FILTERS, LIST-STATUS, + SORT=DISPLAY, SPECIAL-USE, CREATE-SPECIAL-USE, SEARCH=FUZZY, MOVE, UTF8=ACCEPT, UTF8=ONLY, LITERAL-, + APPENDLIMIT, STATUS=SIZE, OBJECTID, REPLACE, SAVEDATE, XLIST, and Google Mail (X-GM-EXT-1) extensions. + + + + + Proxy Support + + + Supports HTTP(S), SOCKS4, SOCKS4a, and SOCKS5. + + + Fully cancellable and asynchronous Connect methods. @@ -84,7 +98,7 @@ - All API's that might block allow canellation via T:System.Threading.CancellationToken. + All API's that might block allow cancellation via T:System.Threading.CancellationToken. @@ -94,7 +108,7 @@ Client-side sorting and threading of messages. - Supports .NET 4.0, .NET 4.5, .NETStandard 1.3, Xamarin.Android, Xamarin.iOS, Windows Phone 8.1, and more. + Supports .NET 4.5.2, .NET 4.6, .NET 4.7, .NET 4.8, .NET 5.0, .NETStandard 2.0, Xamarin.Android, Xamarin.iOS, Windows Phone 8.1, and more. @@ -172,8 +186,12 @@ MimeKit and MailKit are personal open source projects that I have put thousands of hours into perfecting by continuously improving the API based on feedback from developers like yourself, writing documentation, - and optimizing with the goal of making them not only the very best email framework for .NET, but the best - email framework for any programming language. I need your help to achieve this. + and optimizing with the goal of making them the very best email frameworks for .NET. I need your help to + achieve this. + + + Donating helps pay for things such as web hosting, domain registration and licenses for developer tools + such as a performance profiler, memory profiler, a static code analysis tool, and more. If MimeKit and/or MailKit have been helpful to you, please consider donating. Your contributions will be @@ -181,8 +199,8 @@

- - Click here to lend your support to MimeKit and MailKit by making a donation via pledgie.com! + + Click here to lend your support to MimeKit and MailKit by making a donation!

diff --git a/Documentation/Content/License.aml b/Documentation/Content/License.aml index 9b57d501f7..6bb0c66a72 100644 --- a/Documentation/Content/License.aml +++ b/Documentation/Content/License.aml @@ -45,7 +45,7 @@ Copyright Notices - MimeKit and MailKit are Copyright © 2013-2017 Jeffrey Stedfast + MimeKit and MailKit are Copyright © 2013-2026 Jeffrey Stedfast diff --git a/Documentation/Content/Parsing-Messages.aml b/Documentation/Content/Parsing-Messages.aml index fdb7dbb312..b9db47b1d8 100644 --- a/Documentation/Content/Parsing-Messages.aml +++ b/Documentation/Content/Parsing-Messages.aml @@ -29,7 +29,7 @@ - +
Using MimeParser directly @@ -38,9 +38,9 @@ For the most part, using the MimeParser directly is not necessary unless you wish to parse a Unix mbox file stream. However, this is how you would do it: - + For Unix mbox file streams, you would use the parser like this: - +
diff --git a/Documentation/Content/Working-With-Messages.aml b/Documentation/Content/Working-With-Messages.aml index 3418cddbb9..0319440c2a 100644 --- a/Documentation/Content/Working-With-Messages.aml +++ b/Documentation/Content/Working-With-Messages.aml @@ -1,4 +1,4 @@ - + - MimeKit provies a number of ways to get the data you want from a message. + MimeKit provides a number of ways to get the data you want from a message. @@ -206,7 +206,7 @@ multipart/alternative You can also get access to the original raw content by "opening" the - P:MimeKit.MimePart.ContentObject. + P:MimeKit.MimePart.Content. This might be useful if you want to pass the content off to a UI control that can do its own loading from a stream. diff --git a/Documentation/Content/Working-With-OpenPGP.aml b/Documentation/Content/Working-With-OpenPGP.aml index 272222f5ba..90a6d0df99 100644 --- a/Documentation/Content/Working-With-OpenPGP.aml +++ b/Documentation/Content/Working-With-OpenPGP.aml @@ -1,4 +1,4 @@ - + - +
Creating your own OpenPGP Context @@ -49,9 +49,9 @@ PGP/MIME uses a MIME part with a multipart/encrypted mime-type to encapsulate encrypted data. To encrypt any T:MimeKit.MimeEntity, - simply use the - - Overload:MimeKit.Cryptography.MultipartEncrypted.Create + use the + + Overload:MimeKit.Cryptography.MultipartEncrypted.Encrypt method: @@ -85,7 +85,7 @@ The first thing you must do is find the T:MimeKit.Cryptography.MultipartEncrypted - part (see the section on ). + part (see the section on ). diff --git a/Documentation/Content/Working-With-SMime.aml b/Documentation/Content/Working-With-SMime.aml index ae59d6d336..389bd66320 100644 --- a/Documentation/Content/Working-With-SMime.aml +++ b/Documentation/Content/Working-With-SMime.aml @@ -1,4 +1,4 @@ - + - If you are targetting any of the Xamarin platforms + If you are targeting any of the Xamarin platforms (or Linux), you won't need to do anything (although you certainly can if you want to) because, by default, MimeKit will automatically use the Mono.Data.Sqlite binding to @@ -66,7 +66,7 @@ Instead of using a multipart/encrypted MIME part to encapsulate encrypted content like OpenPGP, S/MIME uses application/pkcs7-mime. To encrypt any T:MimeKit.MimeEntity, - simply use the + use the Overload:MimeKit.Cryptography.ApplicationPkcs7Mime.Encrypt @@ -96,7 +96,7 @@ The first thing you must do is find the T:MimeKit.Cryptography.ApplicationPkcs7Mime - part (see the section on ). + part (see the section on ). @@ -112,7 +112,7 @@ To digitally sign a T:MimeKit.MimeEntity using a multipart/signed MIME part, it works exactly the same - as it does for OpenPGP using + as it does for OpenPGP using Overload:MimeKit.Cryptography.MultipartSigned.Create @@ -124,7 +124,7 @@ - You can also choose to digitially sign a + You can also choose to digitally sign a T:MimeKit.MimeEntity using the application/pkcs7-mime format using diff --git a/Documentation/Documentation.shfbproj b/Documentation/Documentation.shfbproj index 4d28520fe0..ba0f64ae8b 100644 --- a/Documentation/Documentation.shfbproj +++ b/Documentation/Documentation.shfbproj @@ -1,5 +1,5 @@ - - + + @@ -7,25 +7,34 @@ AnyCPU 2.0 59115814-a1e3-46ae-ae30-4065ae8f4caf - 2015.6.5.0 + 2017.9.26.0 Documentation Documentation Documentation - .NET Framework 4.5 + Cross-platform (.NET Core/.NET Standard) bin\docs\ Documentation en-US - - - - - - - - + + + + + + + + + + + + + + + + + OnlyWarningsAndErrors Website @@ -46,10 +55,10 @@ AboveNamespaces API Reference - - - - + + + + The <b>MimeKit</b> namespace provides classes that are used to implement the core MIME parsing services of the framework. @@ -63,18 +72,30 @@ The <b>MailKit</b> namespace provides classes that are used to implement the core services of the framework. The <b>MailKit.Net.Imap</b> namespace provides classes that are necessary for managing messages on an IMAP server. The <b>MailKit.Net.Pop3</b> namespace provides classes that are necessary for downloading messages from a POP3 server. + The <b>MailKit.Net.Proxy</b> namespace provides classes that are necessary for connecting via proxy servers. The <b>MailKit.Net.Smtp</b> namespace provides classes that are necessary for sending messages to an SMTP server. The <b>MailKit.Search</b> namespace provides classes that are necessary for searching folders for messages matching a set of criteria. The <b>MailKit.Security</b> namespace provides implementations of various SASL authentication mechanisms used by the IMAP, POP3 and SMTP clients. - Copyright &#169%3b 2013-2017 Jeffrey Stedfast + Copyright &#169%3b 2013-2026 Jeffrey Stedfast obj\ - &lt%3bscript&gt%3b%28function%28i,s,o,g,r,a,m%29{{i[&#39%3bGoogleAnalyticsObject&#39%3b]=r%3bi[r]=i[r]||function%28%29{{ %28i[r].q=i[r].q||[]%29.push%28arguments%29}},i[r].l=1%2anew Date%28%29%3ba=s.createElement%28o%29, m=s.getElementsByTagName%28o%29[0]%3ba.async=1%3ba.src=g%3bm.parentNode.insertBefore%28a,m%29 }}%29%28window,document,&#39%3bscript&#39%3b,&#39%3b//www.google-analytics.com/analytics.js&#39%3b,&#39%3bga&#39%3b%29%3b ga%28&#39%3bcreate&#39%3b, &#39%3bUA-63841238-1&#39%3b, &#39%3bauto&#39%3b%29%3b ga%28&#39%3bsend&#39%3b, &#39%3bpageview&#39%3b%29%3b&lt%3b/script&gt%3b + &lt%3b!-- Global site tag %28gtag.js%29 - Google Analytics --&gt%3b +&lt%3bscript src=&quot%3bhttps://www.googletagmanager.com/gtag/js%3fid=UA-63841238-1&quot%3b&gt%3b&lt%3b/script&gt%3b +&lt%3bscript&gt%3b + window.dataLayer = window.dataLayer || []%3b + function gtag%28%29{{dataLayer.push%28arguments%29%3b}} + gtag%28&#39%3bjs&#39%3b, new Date%28%29%29%3b + + gtag%28&#39%3bconfig&#39%3b, &#39%3bUA-63841238-1&#39%3b%29%3b +&lt%3b/script&gt%3b 100 1.0.0.0 Jeffrey Stedfast jestedfa%40microsoft.com https://github.com/jstedfast + Summary, Parameter, Returns, Value, Remarks, AutoDocumentCtors, Namespace, TypeParameter, AutoDocumentDispose + v4.8 + @@ -105,23 +126,29 @@ + - + - - + + + + + + + @@ -131,23 +158,6 @@ - - - BouncyCastle - {4c235092-820c-4deb-9074-d356fb797d8b} - True - - - MimeKit.Net45 - {d5f54a4f-d84b-430f-9271-f7861e285b3e} - True - - - MailKit.Net45 - {7264d469-a390-4c10-9c87-daa37edd3c1d} - True - - clone @@ -158,6 +168,18 @@ Update MailKit source code via TortoiseGit using the Pull and Submodule Update buttons. + + + MailKit + {e543a427-93de-4e65-adf2-44412e440fb1} + True + + + MimeKit + {faec8a91-6983-4ed9-a414-09c6b65b13bb} + True + + \ No newline at end of file diff --git a/Documentation/Examples/ArcSignerExample.cs b/Documentation/Examples/ArcSignerExample.cs new file mode 100644 index 0000000000..6050fe6cad --- /dev/null +++ b/Documentation/Examples/ArcSignerExample.cs @@ -0,0 +1,124 @@ +using System; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; + +using MimeKit; +using MimeKit.Cryptography; + +namespace ArcSignerExample +{ + class ExampleArcSigner : ArcSigner + { + public ExampleArcSigner (Stream stream, string domain, string selector, DkimSignatureAlgorithm algorithm = DkimSignatureAlgorithm.RsaSha256) : base (stream, domain, selector, algorithm) + { + } + + public ExampleArcSigner (string fileName, string domain, string selector, DkimSignatureAlgorithm algorithm = DkimSignatureAlgorithm.RsaSha256) : base (fileName, domain, selector, algorithm) + { + } + + public ExampleArcSigner (AsymmetricKeyParameter key, string domain, string selector, DkimSignatureAlgorithm algorithm = DkimSignatureAlgorithm.RsaSha256) : base (key, domain, selector, algorithm) + { + } + + public string AuthenticationServiceIdentifier { + get; set; + } + + /// + /// Generate the ARC-Authentication-Results header. + /// + /// + /// The ARC-Authentication-Results header contains information detailing the results of + /// authenticating/verifying the message via ARC, DKIM, SPF, etc. + /// + /// In the following implementation, we assume that all of these authentication results + /// have already been determined by other mail software that has added some Authentication-Results + /// headers containing this information. + /// + /// Note: This method is used when ArcSigner.Sign() is called instead of ArcSigner.SignAsync(). + /// + protected override AuthenticationResults GenerateArcAuthenticationResults (FormatOptions options, MimeMessage message, CancellationToken cancellationToken) + { + var results = new AuthenticationResults (AuthenticationServiceIdentifier); + + for (int i = 0; i < message.Headers.Count; i++) { + var header = message.Headers[i]; + + if (header.Id != HeaderId.AuthenticationResults) + continue; + + if (!AuthenticationResults.TryParse (header.RawValue, out AuthenticationResults authres)) + continue; + + if (authres.AuthenticationServiceIdentifier != AuthenticationServiceIdentifier) + continue; + + foreach (var result in authres.Results) { + if (!results.Results.Any (r => r.Method == result.Method)) + results.Results.Add (result); + } + } + + return results; + } + + protected override Task GenerateArcAuthenticationResultsAsync (FormatOptions options, MimeMessage message, CancellationToken cancellationToken) + { + return Task.FromResult (GenerateArcAuthenticationResults (options, message, cancellationToken)); + } + } + + class Program + { + public static void Main (string[] args) + { + if (args.Length < 2) { + Help (); + return; + } + + for (int i = 0; i < args.Length; i++) { + if (args[i] == "--help") { + Help (); + return; + } + } + + var headers = new HeaderId[] { HeaderId.From, HeaderId.Subject, HeaderId.Date }; + var signer = new ExampleArcSigner ("privatekey.pem", "example.com", "brisbane", DkimSignatureAlgorithm.RsaSha256) { + HeaderCanonicalizationAlgorithm = DkimCanonicalizationAlgorithm.Simple, + BodyCanonicalizationAlgorithm = DkimCanonicalizationAlgorithm.Simple, + AgentOrUserIdentifier = "@eng.example.com", + }; + + if (!File.Exists (args[0])) { + Console.Error.WriteLine ("{0}: No such file.", args[0]); + return; + } + + var message = MimeMessage.Load (args[0]); + + // Prepare the message body to be sent over a 7bit transport (such as older versions of SMTP). + // Note: If the SMTP server you will be sending the message over supports the 8BITMIME extension, + // then you can use `EncodingConstraint.EightBit` instead. + message.Prepare (EncodingConstraint.SevenBit); + + signer.Sign (message, headers); + + using (var stream = File.Create (args[1])) + message.WriteTo (stream); + } + + static void Help () + { + Console.WriteLine ("Usage is: ArcSigner [options] [message] [output]"); + Console.WriteLine (); + Console.WriteLine ("Options:"); + Console.WriteLine (" --help This help menu."); + } + } +} diff --git a/Documentation/Examples/ArcVerifierExample.cs b/Documentation/Examples/ArcVerifierExample.cs new file mode 100644 index 0000000000..8d0b6dc825 --- /dev/null +++ b/Documentation/Examples/ArcVerifierExample.cs @@ -0,0 +1,139 @@ +using System; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; + +using Heijden.DNS; + +using Org.BouncyCastle.Crypto; + +using MimeKit; +using MimeKit.Cryptography; + +namespace ArcVerifierExample +{ + // Note: By using the DkimPublicKeyLocatorBase, we avoid having to parse the DNS TXT records + // in order to get the public key ourselves. + class ExamplePublicKeyLocator : DkimPublicKeyLocatorBase + { + readonly Dictionary cache; + readonly Resolver resolver; + + public ExamplePublicKeyLocator () + { + cache = new Dictionary (); + + resolver = new Resolver ("8.8.8.8") { + TransportType = TransportType.Udp, + UseCache = true, + Retries = 3 + }; + } + + AsymmetricKeyParameter DnsLookup (string domain, string selector, CancellationToken cancellationToken) + { + var query = selector + "._domainkey." + domain; + AsymmetricKeyParameter pubkey; + + // checked if we've already fetched this key + if (cache.TryGetValue (query, out pubkey)) + return pubkey; + + // make a DNS query + var response = resolver.Query (query, QType.TXT); + var builder = new StringBuilder (); + + // combine the TXT records into 1 string buffer + foreach (var record in response.RecordsTXT) { + foreach (var text in record.TXT) + builder.Append (text); + } + + var txt = builder.ToString (); + + // DkimPublicKeyLocatorBase provides us with this helpful method. + pubkey = GetPublicKey (txt); + + cache.Add (query, pubkey); + + return pubkey; + } + + public AsymmetricKeyParameter LocatePublicKey (string methods, string domain, string selector, CancellationToken cancellationToken = default (CancellationToken)) + { + var methodList = methods.Split (new char[] { ':' }, StringSplitOptions.RemoveEmptyEntries); + for (int i = 0; i < methodList.Length; i++) { + if (methodList[i] == "dns/txt") + return DnsLookup (domain, selector, cancellationToken); + } + + throw new NotSupportedException (string.Format ("{0} does not include any supported lookup methods.", methods)); + } + + public Task LocatePublicKeyAsync (string methods, string domain, string selector, CancellationToken cancellationToken = default (CancellationToken)) + { + return Task.Run (() => { + return LocatePublicKey (methods, domain, selector, cancellationToken); + }, cancellationToken); + } + } + + class Program + { + public static void Main (string[] args) + { + if (args.Length == 0) { + Help (); + return; + } + + for (int i = 0; i < args.Length; i++) { + if (args[i] == "--help") { + Help (); + return; + } + } + + var locator = new ExamplePublicKeyLocator (); + var verifier = new ArcVerifier (locator); + + for (int i = 0; i < args.Length; i++) { + if (!File.Exists (args[i])) { + Console.Error.WriteLine ("{0}: No such file.", args[i]); + continue; + } + + Console.Write ("{0} -> ", args[i]); + + var message = MimeMessage.Load (args[i]); + var result = verifier.Verify (message); + + switch (result.Chain) { + case ArcSignatureValidationResult.None: + Console.WriteLine ("No ARC signatures to verify."); + break; + case ArcSignatureValidationResult.Pass: + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine ("PASS"); + Console.ResetColor (); + break; + case ArcSignatureValidationResult.Fail: + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine ("FAIL"); + Console.ResetColor (); + break; + } + } + } + + static void Help () + { + Console.WriteLine ("Usage is: ArcVerifier [options] [messages]"); + Console.WriteLine (); + Console.WriteLine ("Options:"); + Console.WriteLine (" --help This help menu."); + } + } +} diff --git a/Documentation/Examples/AttachmentExamples.cs b/Documentation/Examples/AttachmentExamples.cs index 6238480da4..3da8f93039 100644 --- a/Documentation/Examples/AttachmentExamples.cs +++ b/Documentation/Examples/AttachmentExamples.cs @@ -10,7 +10,7 @@ public static void SaveMimePart (MimePart attachment, string fileName) { #region SaveMimePart using (var stream = File.Create (fileName)) - attachment.ContentObject.DecodeTo (stream); + attachment.Content.DecodeTo (stream); #endregion SaveMimePart } @@ -27,17 +27,29 @@ public static void SaveAttachments (MimeMessage message) #region SaveAttachments foreach (var attachment in message.Attachments) { if (attachment is MessagePart) { - var fileName = attachment.ContentDisposition?.FileName : - (attachment.ContentType.Name ?? "attached.eml"); + var fileName = attachment.ContentDisposition?.FileName ?? attachment.ContentType.Name; var rfc822 = (MessagePart) attachment; - rfc822.Message.WriteTo (stream); + if (string.IsNullOrEmpty (fileName)) + fileName = "attached-message.eml"; + + // make sure that the filename value does not contain a full path or invalid path characters + fileName = Path.GetFileName (fileName); + + using (var stream = File.Create (fileName)) + rfc822.Message.WriteTo (stream); } else { var part = (MimePart) attachment; var fileName = part.FileName; + if (string.IsNullOrEmpty (fileName)) + fileName = "untitled.dat"; + + // make sure that the filename value does not contain a full path or invalid path characters + fileName = Path.GetFileName (fileName); + using (var stream = File.Create (fileName)) - part.ContentObject.DecodeTo (stream); + part.Content.DecodeTo (stream); } } #endregion SaveAttachments @@ -51,17 +63,29 @@ public static void SaveAttachments (MimeMessage message) continue; if (bodyPart is MessagePart) { - var fileName = bodyPart.ContentDisposition?.FileName : - (bodyPart.ContentType.Name ?? "attached.eml"); - var rfc822 = (MessagePart) bodyPart; + var fileName = attachment.ContentDisposition?.FileName ?? attachment.ContentType.Name; + var rfc822 = (MessagePart) attachment; - rfc822.Message.WriteTo (stream); + if (string.IsNullOrEmpty (fileName)) + fileName = "attached-message.eml"; + + // make sure that the filename value does not contain a full path or invalid path characters + fileName = Path.GetFileName (fileName); + + using (var stream = File.Create (fileName)) + rfc822.Message.WriteTo (stream); } else { var part = (MimePart) attachment; var fileName = part.FileName; + if (string.IsNullOrEmpty (fileName)) + fileName = "untitled.dat"; + + // make sure that the filename value does not contain a full path or invalid path characters + fileName = Path.GetFileName (fileName); + using (var stream = File.Create (fileName)) - part.ContentObject.DecodeTo (stream); + part.Content.DecodeTo (stream); } } #endregion SaveBodyParts diff --git a/Documentation/Examples/CreateMultipartAlternative.cs b/Documentation/Examples/CreateMultipartAlternative.cs index cca41d260d..4067578e23 100644 --- a/Documentation/Examples/CreateMultipartAlternative.cs +++ b/Documentation/Examples/CreateMultipartAlternative.cs @@ -5,7 +5,7 @@ // Note: it is important that the text/html part is added second, because it is the // most expressive version and (probably) the most faithful to the sender's WYSIWYG // editor. -var alternative = new Multipart ("alternative"); +var alternative = new MultipartAlternative (); alternative.Add (plain); alternative.Add (html); diff --git a/Documentation/Examples/CreateMultipartMixed.cs b/Documentation/Examples/CreateMultipartMixed.cs index f05f700e45..4a5b55df07 100644 --- a/Documentation/Examples/CreateMultipartMixed.cs +++ b/Documentation/Examples/CreateMultipartMixed.cs @@ -18,7 +18,7 @@ Will you be my +1? // create an image attachment for the file located at path var attachment = new MimePart ("image", "gif") { - ContentObject = new ContentObject (File.OpenRead (path), ContentEncoding.Default), + Content = new MimeContent (File.OpenRead (path), ContentEncoding.Default), ContentDisposition = new ContentDisposition (ContentDisposition.Attachment), ContentTransferEncoding = ContentEncoding.Base64, FileName = Path.GetFileName (path) diff --git a/Documentation/Examples/DecodingContent.cs b/Documentation/Examples/DecodingContent.cs index c0a4c9b8d8..0ba86fdf83 100644 --- a/Documentation/Examples/DecodingContent.cs +++ b/Documentation/Examples/DecodingContent.cs @@ -3,5 +3,5 @@ var fileName = part.FileName; using (var stream = File.Create (fileName)) { - part.ContentObject.DecodeTo (stream); + part.Content.DecodeTo (stream); } diff --git a/Documentation/Examples/DkimExamples.cs b/Documentation/Examples/DkimExamples.cs index fe7348ad27..aec9f71e8c 100644 --- a/Documentation/Examples/DkimExamples.cs +++ b/Documentation/Examples/DkimExamples.cs @@ -11,10 +11,9 @@ public static class DkimExamples public static void DkimSign (MimeMessage message) { var headers = new HeaderId[] { HeaderId.From, HeaderId.Subject, HeaderId.Date }; - var headerAlgorithm = DkimCanonicalizationAlgorithm.Simple; - var bodyAlgorithm = DkimCanonicalizationAlgorithm.Simple; - var signer = new DkimSigner ("privatekey.pem") { - SignatureAlgorithm = DkimSignatureAlgorithm.RsaSha1, + var signer = new DkimSigner ("privatekey.pem", "example.com", "brisbane", DkimSignatureAlgorithm.RsaSha256) { + HeaderCanonicalizationAlgorithm = DkimCanonicalizationAlgorithm.Simple, + BodyCanonicalizationAlgorithm = DkimCanonicalizationAlgorithm.Simple, AgentOrUserIdentifier = "@eng.example.com", QueryMethod = "dns/txt", }; @@ -24,8 +23,8 @@ public static void DkimSign (MimeMessage message) // then you can use `EncodingConstraint.EightBit` instead. message.Prepare (EncodingConstraint.SevenBit); - message.Sign (signer, headers, headerAlgorithm, bodyAlgorithm); + signer.Sign (message, headers); } #endregion } -} \ No newline at end of file +} diff --git a/Documentation/Examples/DkimVerifierExample.cs b/Documentation/Examples/DkimVerifierExample.cs index 58bbce427c..8926e37c61 100644 --- a/Documentation/Examples/DkimVerifierExample.cs +++ b/Documentation/Examples/DkimVerifierExample.cs @@ -2,24 +2,26 @@ using System.IO; using System.Text; using System.Threading; +using System.Threading.Tasks; using System.Collections.Generic; using Heijden.DNS; using Org.BouncyCastle.Crypto; -using Org.BouncyCastle.OpenSsl; using MimeKit; using MimeKit.Cryptography; -namespace DkimVerifier +namespace DkimVerifierExample { - class DkimPublicKeyLocator : IDkimPublicKeyLocator + // Note: By using the DkimPublicKeyLocatorBase, we avoid having to parse the DNS TXT records + // in order to get the public key ourselves. + class ExamplePublicKeyLocator : DkimPublicKeyLocatorBase { readonly Dictionary cache; readonly Resolver resolver; - public DkimPublicKeyLocator () + public ExamplePublicKeyLocator () { cache = new Dictionary (); @@ -50,66 +52,13 @@ AsymmetricKeyParameter DnsLookup (string domain, string selector, CancellationTo } var txt = builder.ToString (); - string k = null, p = null; - int index = 0; - // parse the response (will look something like: "k=rsa; p=") - while (index < txt.Length) { - while (index < txt.Length && char.IsWhiteSpace (txt[index])) - index++; + // DkimPublicKeyLocatorBase provides us with this helpful method. + pubkey = GetPublicKey (txt); - if (index == txt.Length) - break; + cache.Add (query, pubkey); - // find the end of the key - int startIndex = index; - while (index < txt.Length && txt[index] != '=') - index++; - - if (index == txt.Length) - break; - - var key = txt.Substring (startIndex, index - startIndex); - - // skip over the '=' - index++; - - // find the end of the value - startIndex = index; - while (index < txt.Length && txt[index] != ';') - index++; - - var value = txt.Substring (startIndex, index - startIndex); - - switch (key) { - case "k": k = value; break; - case "p": p = value; break; - } - - // skip over the ';' - index++; - } - - if (k != null && p != null) { - var data = "-----BEGIN PUBLIC KEY-----\r\n" + p + "\r\n-----END PUBLIC KEY-----\r\n"; - var rawData = Encoding.ASCII.GetBytes (data); - - using (var stream = new MemoryStream (rawData, false)) { - using (var reader = new StreamReader (stream)) { - var pem = new PemReader (reader); - - pubkey = pem.ReadObject () as AsymmetricKeyParameter; - - if (pubkey != null) { - cache.Add (query, pubkey); - - return pubkey; - } - } - } - } - - throw new Exception (string.Format ("Failed to look up public key for: {0}", domain)); + return pubkey; } public AsymmetricKeyParameter LocatePublicKey (string methods, string domain, string selector, CancellationToken cancellationToken = default (CancellationToken)) @@ -120,13 +69,20 @@ AsymmetricKeyParameter DnsLookup (string domain, string selector, CancellationTo return DnsLookup (domain, selector, cancellationToken); } - throw new NotSupportedException (string.Format ("{0} does not include any suported lookup methods.", methods)); + throw new NotSupportedException (string.Format ("{0} does not include any supported lookup methods.", methods)); + } + + public Task LocatePublicKeyAsync (string methods, string domain, string selector, CancellationToken cancellationToken = default (CancellationToken)) + { + return Task.Run (() => { + return LocatePublicKey (methods, domain, selector, cancellationToken); + }, cancellationToken); } } class Program { - public static void Main(string[] args) + public static void Main (string[] args) { if (args.Length == 0) { Help (); @@ -140,7 +96,8 @@ public static void Main(string[] args) } } - var locator = new DkimPublicKeyLocator (); + var locator = new ExamplePublicKeyLocator (); + var verifier = new DkimVerifier (locator); for (int i = 0; i < args.Length; i++) { if (!File.Exists (args[i])) { @@ -160,7 +117,7 @@ public static void Main(string[] args) var dkim = message.Headers[index]; - if (message.Verify (dkim, locator)) { + if (verifier.Verify (message, dkim)) { // the DKIM-Signature header is valid! Console.ForegroundColor = ConsoleColor.Green; Console.WriteLine ("VALID"); diff --git a/Documentation/Examples/ForwardExamples.cs b/Documentation/Examples/ForwardExamples.cs index a7ca86cd09..b7d4823a27 100644 --- a/Documentation/Examples/ForwardExamples.cs +++ b/Documentation/Examples/ForwardExamples.cs @@ -1,9 +1,9 @@ // -// MimeVisitorExamples.cs +// ForwardExamples.cs // -// Author: Jeffrey Stedfast +// Author: Jeffrey Stedfast // -// Copyright (c) 2013-2016 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2023 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -44,8 +44,8 @@ public static MimeMessage Forward (MimeMessage original, MailboxAddress from, IE message.To.AddRange (to); // set the forwarded subject - if (!original.Subject.StartsWith ("FW:", StringComparison.OrdinalIgnoreCase)) - message.Subject = "FW: " + original.Subject; + if (!original.Subject?.StartsWith ("FW:", StringComparison.OrdinalIgnoreCase)) + message.Subject = "FW: " + (original.Subject ?? string.Empty); else message.Subject = original.Subject; @@ -75,8 +75,8 @@ public static MimeMessage Forward (MimeMessage original, MailboxAddress from, IE message.To.AddRange (to); // set the forwarded subject - if (!original.Subject.StartsWith ("FW:", StringComparison.OrdinalIgnoreCase)) - message.Subject = "FW: " + original.Subject; + if (!original.Subject?.StartsWith ("FW:", StringComparison.OrdinalIgnoreCase)) + message.Subject = "FW: " + (original.Subject ?? string.Empty); else message.Subject = original.Subject; @@ -87,7 +87,7 @@ public static MimeMessage Forward (MimeMessage original, MailboxAddress from, IE test.WriteLine ("From: {0}", original.From); text.WriteLine ("Sent: {0}", DateUtils.FormatDate (original.Date)); text.WriteLine ("To: {0}", original.To); - text.WriteLine ("Subject: {0}", original.Subject); + text.WriteLine ("Subject: {0}", original.Subject ?? string.Empty); text.WriteLine (); text.Write (original.TextBody); diff --git a/Documentation/Examples/ImapBodyPartExamples.cs b/Documentation/Examples/ImapBodyPartExamples.cs new file mode 100644 index 0000000000..ddf81bc795 --- /dev/null +++ b/Documentation/Examples/ImapBodyPartExamples.cs @@ -0,0 +1,340 @@ +// +// ImapBodyPartExamples.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2023 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Collections; +using System.Collections.Generic; + +using MimeKit; +using MailKit; +using MailKit.Search; +using MailKit.Security; +using MailKit.Net.Imap; + +namespace MailKit.Examples { + public static class ImapBodyPartExamples + { + #region GetBodyPartsByUniqueId + public static void DownloadBodyAndAttachments (string baseDirectory) + { + using (var client = new ImapClient ()) { + client.Connect ("imap.gmail.com", 993, SecureSocketOptions.SslOnConnect); + + client.Authenticate ("username", "password"); + + client.Inbox.Open (FolderAccess.ReadOnly); + + // search for messages where the Subject header contains either "MimeKit" or "MailKit" + var query = SearchQuery.SubjectContains ("MimeKit").Or (SearchQuery.SubjectContains ("MailKit")); + var uids = client.Inbox.Search (query); + + // fetch summary information for the search results (we will want the UID and the BODYSTRUCTURE + // of each message so that we can extract the text body and the attachments) + var items = client.Inbox.Fetch (uids, MessageSummaryItems.UniqueId | MessageSummaryItems.BodyStructure); + + foreach (var item in items) { + // determine a directory to save stuff in + var directory = Path.Combine (baseDirectory, item.UniqueId.ToString ()); + + // create the directory + Directory.CreateDirectory (directory); + + // IMessageSummary.TextBody is a convenience property that finds the 'text/plain' body part for us + var bodyPart = item.TextBody; + + if (bodyPart != null) { + // download the 'text/plain' body part + var plain = (TextPart) client.Inbox.GetBodyPart (item.UniqueId, bodyPart); + + // TextPart.Text is a convenience property that decodes the content and converts the result to + // a string for us + var text = plain.Text; + + File.WriteAllText (Path.Combine (directory, "body.txt"), text); + } + + // IMessageSummary.HtmlBody is a convenience property that finds the 'text/html' body part for us + bodyPart = item.HtmlBody; + + if (bodyPart != null) { + // download the 'text/html' body part + var html = (TextPart) client.Inbox.GetBodyPart (item.UniqueId, bodyPart); + + // TextPart.Text is a convenience property that decodes the content and converts the result to + // a string for us + var text = html.Text; + + File.WriteAllText (Path.Combine (directory, "body.html"), text); + } + + // now iterate over all of the attachments and save them to disk + foreach (var attachment in item.Attachments) { + // download the attachment just like we did with the body + var entity = client.Inbox.GetBodyPart (item.UniqueId, attachment); + + // attachments can be either message/rfc822 parts or regular MIME parts + if (entity is MessagePart) { + var rfc822 = (MessagePart) entity; + + var path = Path.Combine (directory, attachment.PartSpecifier + ".eml"); + + rfc822.Message.WriteTo (path); + } else { + var part = (MimePart) entity; + + // default to using the sending client's suggested fileName value + var fileName = attachment.FileName; + + if (string.IsNullOrEmpty (fileName)) { + // the FileName wasn't defined, so generate one... + if (!MimeTypes.TryGetExtension (attachment.ContentType.MimeType, out string extension)) + extension = ".dat"; + + fileName = Guid.NewGuid ().ToString () + extension; + } + + var path = Path.Combine (directory, fileName); + + // decode and save the content to a file + using (var stream = File.Create (path)) + part.Content.DecodeTo (stream); + } + } + } + + client.Disconnect (true); + } + } + #endregion + + #region GetBodyPartsByUniqueIdAndSpecifier + public static void DownloadBodyAndAttachments (string baseDirectory) + { + using (var client = new ImapClient ()) { + client.Connect ("imap.gmail.com", 993, SecureSocketOptions.SslOnConnect); + + client.Authenticate ("username", "password"); + + client.Inbox.Open (FolderAccess.ReadOnly); + + // search for messages where the Subject header contains either "MimeKit" or "MailKit" + var query = SearchQuery.SubjectContains ("MimeKit").Or (SearchQuery.SubjectContains ("MailKit")); + var uids = client.Inbox.Search (query); + + // fetch summary information for the search results (we will want the UID and the BODYSTRUCTURE + // of each message so that we can extract the text body and the attachments) + var items = client.Inbox.Fetch (uids, MessageSummaryItems.UniqueId | MessageSummaryItems.BodyStructure); + + foreach (var item in items) { + // determine a directory to save stuff in + var directory = Path.Combine (baseDirectory, item.UniqueId.ToString ()); + + // create the directory + Directory.CreateDirectory (directory); + + // IMessageSummary.TextBody is a convenience property that finds the 'text/plain' body part for us + var bodyPart = item.TextBody; + + if (bodyPart != null) { + // download the 'text/plain' body part + + // Note: In general, you should use `GetBodyPart(item.UniqueId, bodyPart)` instead if you have it available. + // This particular overload of the GetBodyPart() method exists for convenience purposes where you already + // know the body-part specifier string before-hand. + var plain = (TextPart) client.Inbox.GetBodyPart (item.UniqueId, bodyPart.PartSpecifier); + + // TextPart.Text is a convenience property that decodes the content and converts the result to + // a string for us + var text = plain.Text; + + File.WriteAllText (Path.Combine (directory, "body.txt"), text); + } + + // IMessageSummary.HtmlBody is a convenience property that finds the 'text/html' body part for us + bodyPart = item.HtmlBody; + + if (bodyPart != null) { + // download the 'text/html' body part + + // Note: In general, you should use `GetBodyPart(item.UniqueId, bodyPart)` instead if you have it available. + // This particular overload of the GetBodyPart() method exists for convenience purposes where you already + // know the body-part specifier string before-hand. + var html = (TextPart) client.Inbox.GetBodyPart (item.UniqueId, bodyPart.PartSpecifier); + + // TextPart.Text is a convenience property that decodes the content and converts the result to + // a string for us + var text = html.Text; + + File.WriteAllText (Path.Combine (directory, "body.html"), text); + } + + // now iterate over all of the attachments and save them to disk + foreach (var attachment in item.Attachments) { + // download the attachment just like we did with the body + var entity = client.Inbox.GetBodyPart (item.UniqueId, attachment); + + // attachments can be either message/rfc822 parts or regular MIME parts + if (entity is MessagePart) { + var rfc822 = (MessagePart) entity; + + var path = Path.Combine (directory, attachment.PartSpecifier + ".eml"); + + rfc822.Message.WriteTo (path); + } else { + var part = (MimePart) entity; + + // default to using the sending client's suggested fileName value + var fileName = attachment.FileName; + + if (string.IsNullOrEmpty (fileName)) { + // the FileName wasn't defined, so generate one... + if (!MimeTypes.TryGetExtension (attachment.ContentType.MimeType, out string extension)) + extension = ".dat"; + + fileName = Guid.NewGuid ().ToString () + extension; + } + + var path = Path.Combine (directory, fileName); + + // decode and save the content to a file + using (var stream = File.Create (path)) + part.Content.DecodeTo (stream); + } + } + } + + client.Disconnect (true); + } + } + #endregion + + #region GetBodyPartStreamsByUniqueId + public static void CacheBodyParts (string baseDirectory) + { + using (var client = new ImapClient ()) { + client.Connect ("imap.gmail.com", 993, SecureSocketOptions.SslOnConnect); + + client.Authenticate ("username", "password"); + + client.Inbox.Open (FolderAccess.ReadOnly); + + // search for messages where the Subject header contains either "MimeKit" or "MailKit" + var query = SearchQuery.SubjectContains ("MimeKit").Or (SearchQuery.SubjectContains ("MailKit")); + var uids = client.Inbox.Search (query); + + // fetch summary information for the search results (we will want the UID and the BODYSTRUCTURE + // of each message so that we can extract the text body and the attachments) + var items = client.Inbox.Fetch (uids, MessageSummaryItems.UniqueId | MessageSummaryItems.BodyStructure); + + foreach (var item in items) { + // determine a directory to save stuff in + var directory = Path.Combine (baseDirectory, item.UniqueId.ToString ()); + + // create the directory + Directory.CreateDirectory (directory); + + // now iterate over all of the body parts and save them to disk + foreach (var bodyPart in item.BodyParts) { + // cache the raw body part MIME just like we did with the body + using (var stream = client.Inbox.GetStream (item.UniqueId, bodyPart)) { + var path = Path.Combine (directory, bodyPart.PartSpecifier); + + using (var output = File.Create (path)) + stream.CopyTo (output); + } + } + } + + client.Disconnect (true); + } + } + #endregion + + #region GetBodyPartStreamsByUniqueIdAndSpecifier + public static void SaveAttachments (string baseDirectory) + { + using (var client = new ImapClient ()) { + client.Connect ("imap.gmail.com", 993, SecureSocketOptions.SslOnConnect); + + client.Authenticate ("username", "password"); + + client.Inbox.Open (FolderAccess.ReadOnly); + + // search for messages where the Subject header contains either "MimeKit" or "MailKit" + var query = SearchQuery.SubjectContains ("MimeKit").Or (SearchQuery.SubjectContains ("MailKit")); + var uids = client.Inbox.Search (query); + + // fetch summary information for the search results (we will want the UID and the BODYSTRUCTURE + // of each message so that we can extract the text body and the attachments) + var items = client.Inbox.Fetch (uids, MessageSummaryItems.UniqueId | MessageSummaryItems.BodyStructure); + + foreach (var item in items) { + // determine a directory to save stuff in + var directory = Path.Combine (baseDirectory, item.UniqueId.ToString ()); + + // create the directory + Directory.CreateDirectory (directory); + + // now iterate over all of the attachments and decode/save the content to disk + foreach (var attachment in item.Attachments) { + // default to using the sending client's suggested fileName value + string fileName = attachment.FileName; + + if (string.IsNullOrEmpty (fileName)) { + // the FileName wasn't defined, so generate one... + if (!MimeTypes.TryGetExtension (attachment.ContentType.MimeType, out string extension)) + extension = ".dat"; + + fileName = Guid.NewGuid ().ToString () + extension; + } + + // we'll need the Content-Transfer-Encoding value so that we can decode it... + ContentEncoding encoding; + + if (string.IsNullOrEmpty (attachment.ContentTransferEncoding) || !MimeUtils.TryParse (attachment.ContentTransferEncoding, out encoding)) + encoding = ContentEncoding.Default; + + // if all we want is the content (rather than the entire MIME part including the headers), then + // we want the ".TEXT" section of the part + using (var stream = client.Inbox.GetStream (item.UniqueId, attachment.PartSpecifier + ".TEXT")) { + // wrap the attachment content in a MimeContent object to help us decode it + using (var content = new MimeContent (stream, encoding)) { + var path = Path.Combine (directory, fileName); + + // decode the attachment content to the file stream + using (var output = File.Create (path)) + content.DecodeTo (output); + } + } + } + } + + client.Disconnect (true); + } + } + #endregion + } +} diff --git a/Documentation/Examples/ImapExamples.cs b/Documentation/Examples/ImapExamples.cs index db3e652dce..626c10ee46 100644 --- a/Documentation/Examples/ImapExamples.cs +++ b/Documentation/Examples/ImapExamples.cs @@ -1,9 +1,9 @@ // // ImapExamples.cs // -// Author: Jeffrey Stedfast +// Author: Jeffrey Stedfast // -// Copyright (c) 2013-2016 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2023 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -101,10 +101,10 @@ public static void Capabilities () Console.WriteLine ("The current quota for the Inbox is:"); var quota = client.Inbox.GetQuota (); - if (quota.StorageLimit.HasValue && quota.StorageLimit.Value) + if (quota.StorageLimit.HasValue) Console.WriteLine (" Limited by storage space. Using {0} out of {1} bytes.", quota.CurrentStorageSize.Value, quota.StorageLimit.Value); - if (quota.MessageLimit.HasValue && quota.MessageLimit.Value) + if (quota.MessageLimit.HasValue) Console.WriteLine (" Limited by the number of messages. Using {0} out of {1} bytes.", quota.CurrentMessageCount.Value, quota.MessageLimit.Value); Console.WriteLine ("The quota root is: {0}", quota.QuotaRoot); @@ -122,7 +122,42 @@ public static void Capabilities () } #endregion - #region DownloadMessages + #region Namespaces + public static void ShowNamespaces () + { + using (var client = new ImapClient ()) { + client.Connect ("imap.mail-server.com", 993, SecureSocketOptions.SslOnConnect); + client.Authenticate ("username", "password"); + + Console.WriteLine ("Personal namespaces:"); + foreach (var ns in client.PersonalNamespaces) + Console.WriteLine ($"* \"{ns.Path}\" \"{ns.DirectorySeparator}\""); + Console.WriteLine (); + Console.WriteLine ("Shared namespaces:"); + foreach (var ns in client.SharedNamespaces) + Console.WriteLine ($"* \"{ns.Path}\" \"{ns.DirectorySeparator}\""); + Console.WriteLine (); + Console.WriteLine ("Other namespaces:"); + foreach (var ns in client.OtherNamespaces) + Console.WriteLine ($"* \"{ns.Path}\" \"{ns.DirectorySeparator}\""); + Console.WriteLine (); + + // get the folder that represents the first personal namespace + var personal = client.GetFolder (client.PersonalNamespaces[0]); + + // list the folders under the first personal namespace + var subfolders = personal.GetSubfolders (); + + Console.WriteLine ("The list of folders that are direct children of the first personal namespace:"); + foreach (var folder in subfolders) + Console.WriteLine ($"* {folder.Name}"); + + client.Disconnect (true); + } + } + #endregion + + #region DownloadMessagesByUniqueId public static void DownloadMessages () { using (var client = new ImapClient ()) { @@ -146,8 +181,8 @@ public static void DownloadMessages () } #endregion - #region DownloadBodyParts - public static void DownloadBodyParts () + #region DownloadMessageStreamsByUniqueId + public static void DownloadMessages () { using (var client = new ImapClient ()) { client.Connect ("imap.gmail.com", 993, SecureSocketOptions.SslOnConnect); @@ -156,57 +191,56 @@ public static void DownloadBodyParts () client.Inbox.Open (FolderAccess.ReadOnly); - // search for messages where the Subject header contains either "MimeKit" or "MailKit" - var query = SearchQuery.SubjectContains ("MimeKit").Or (SearchQuery.SubjectContains ("MailKit")); - var uids = client.Inbox.Search (query); - - // fetch summary information for the search results (we will want the UID and the BODYSTRUCTURE - // of each message so that we can extract the text body and the attachments) - var items = client.Inbox.Fetch (uids, MessageSummaryItems.UniqueId | MessageSummaryItems.BodyStructure); - - foreach (var item in items) { - // determine a directory to save stuff in - var directory = Path.Combine (baseDirectory, item.UniqueId.ToString ()); + var uids = client.Inbox.Search (SearchQuery.All); - // create the directory - Directory.CreateDirectory (directory); + foreach (var uid in uids) { + using (var stream = client.Inbox.GetStream (uid)) { + using (var output = File.Create ($"{uid}.eml")) + stream.CopyTo (output); + } + } - // IMessageSummary.TextBody is a convenience property that finds the 'text/plain' body part for us - var bodyPart = item.TextBody; + client.Disconnect (true); + } + } + #endregion - // download the 'text/plain' body part - var body = (TextPart) client.Inbox.GetBodyPart (item.UniqueId, bodyPart); + #region DownloadMessagesByIndex + public static void DownloadMessages () + { + using (var client = new ImapClient ()) { + client.Connect ("imap.gmail.com", 993, SecureSocketOptions.SslOnConnect); - // TextPart.Text is a convenience property that decodes the content and converts the result to - // a string for us - var text = body.Text; + client.Authenticate ("username", "password"); - File.WriteAllText (Path.Combine (directory, "body.txt"), text); + client.Inbox.Open (FolderAccess.ReadOnly); - // now iterate over all of the attachments and save them to disk - foreach (var attachment in item.Attachments) { - // download the attachment just like we did with the body - var entity = client.Inbox.GetBodyPart (item.UniqueId, attachment); + for (int index = 0; index < client.Inbox.Count; index++) { + var message = client.Inbox.GetMessage (index); - // attachments can be either message/rfc822 parts or regular MIME parts - if (entity is MessagePart) { - var rfc822 = (MessagePart) entity; + // write the message to a file + message.WriteTo (string.Format ("{0}.eml", index)); + } - var path = Path.Combine (directory, attachment.PartSpecifier + ".eml"); + client.Disconnect (true); + } + } + #endregion - rfc822.Message.WriteTo (path); - } else { - var part = (MimePart) entity; + #region DownloadMessageStreamsByIndex + public static void DownloadMessages () + { + using (var client = new ImapClient ()) { + client.Connect ("imap.gmail.com", 993, SecureSocketOptions.SslOnConnect); - // note: it's possible for this to be null, but most will specify a filename - var fileName = part.FileName; + client.Authenticate ("username", "password"); - var path = Path.Combine (directory, fileName); + client.Inbox.Open (FolderAccess.ReadOnly); - // decode and save the content to a file - using (var stream = File.Create (path)) - part.ContentObject.DecodeTo (stream); - } + for (int index = 0; index < client.Inbox.Count; index++) { + using (var stream = client.Inbox.GetStream (index)) { + using (var output = File.Create ($"{index}.eml")) + stream.CopyTo (output); } } @@ -214,5 +248,36 @@ public static void DownloadBodyParts () } } #endregion + + #region SslConnectionInformation + public static void PrintSslConnectionInfo (string host, int port) + { + using (var client = new ImapClient ()) { + client.Connect (host, port, SecureSocketOptions.Auto); + + Console.WriteLine ($"Negotiated the following SSL options with {host}:"); + Console.WriteLine ($" Protocol Version: {client.SslProtocol}"); + Console.WriteLine ($" Cipher Algorithm: {client.SslCipherAlgorithm}"); + Console.WriteLine ($" Cipher Strength: {client.SslCipherStrength}"); + Console.WriteLine ($" Hash Algorithm: {client.SslHashAlgorithm}"); + Console.WriteLine ($" Hash Strength: {client.SslHashStrength}"); + Console.WriteLine ($" Key-Exchange Algorithm: {client.SslKeyExchangeAlgorithm}"); + Console.WriteLine ($" Key-Exchange Strength: {client.SslKeyExchangeStrength}"); + + // Example Log: + // + // Negotiated the following SSL options with imap.gmail.com: + // Protocol Version: Tls12 + // Cipher Algorithm: Aes128 + // Cipher Strength: 128 + // Hash Algorithm: Sha256 + // Hash Strength: 0 + // Key-Exchange Algorithm: 44550 + // Key-Exchange Strength: 255 + + client.Disconnect (true); + } + } + #endregion } } diff --git a/Documentation/Examples/ImapIdleExample.cs b/Documentation/Examples/ImapIdleExample.cs index 805e9f317a..423c4bb7a4 100644 --- a/Documentation/Examples/ImapIdleExample.cs +++ b/Documentation/Examples/ImapIdleExample.cs @@ -1,239 +1,251 @@ using System; -using System.Net; -using System.Linq; +using System.IO; using System.Threading; +using System.Threading.Tasks; using System.Collections.Generic; -using MailKit.Net.Imap; using MailKit; +using MailKit.Net.Imap; +using MailKit.Security; namespace ImapIdleExample { class Program { + // Connection-related properties + const SecureSocketOptions SslOptions = SecureSocketOptions.Auto; + const string Host = "imap.gmail.com"; + const int Port = 993; + + // Authentication-related properties + const string Username = "username@gmail.com"; + const string Password = "password"; + public static void Main (string[] args) { - using (var client = new ImapClient (new ProtocolLogger (Console.OpenStandardError ()))) { - client.Connect ("imap.gmail.com", 993, true); + using (var client = new IdleClient (Host, Port, SslOptions, Username, Password)) { + Console.WriteLine ("Hit any key to end the demo."); - // Remove the XOAUTH2 authentication mechanism since we don't have an OAuth2 token. - client.AuthenticationMechanisms.Remove ("XOAUTH2"); + var idleTask = client.RunAsync (); - client.Authenticate ("username@gmail.com", "password"); + Task.Run (() => { + Console.ReadKey (true); + }).Wait (); - client.Inbox.Open (FolderAccess.ReadOnly); + client.Exit (); - // Get the summary information of all of the messages (suitable for displaying in a message list). - var messages = client.Inbox.Fetch (0, -1, MessageSummaryItems.Full | MessageSummaryItems.UniqueId).ToList (); + idleTask.GetAwaiter ().GetResult (); + } + } + } - // Keep track of messages being expunged so that when the CountChanged event fires, we can tell if it's - // because new messages have arrived vs messages being removed (or some combination of the two). - client.Inbox.MessageExpunged += (sender, e) => { - var folder = (ImapFolder) sender; + class IdleClient : IDisposable + { + readonly string host, username, password; + readonly SecureSocketOptions sslOptions; + readonly int port; + List messages; + CancellationTokenSource cancel; + CancellationTokenSource done; + FetchRequest request; + bool messagesArrived; + ImapClient client; + + public IdleClient (string host, int port, SecureSocketOptions sslOptions, string username, string password) + { + this.client = new ImapClient (new ProtocolLogger (Console.OpenStandardError ())); + this.request = new FetchRequest (MessageSummaryItems.Full | MessageSummaryItems.UniqueId); + this.messages = new List (); + this.cancel = new CancellationTokenSource (); + this.sslOptions = sslOptions; + this.username = username; + this.password = password; + this.host = host; + this.port = port; + } - if (e.Index < messages.Count) { - var message = messages[e.Index]; + async Task ReconnectAsync () + { + if (!client.IsConnected) + await client.ConnectAsync (host, port, sslOptions, cancel.Token); - Console.WriteLine ("{0}: expunged message {1}: Subject: {2}", folder, e.Index, message.Envelope.Subject); + if (!client.IsAuthenticated) { + await client.AuthenticateAsync (username, password, cancel.Token); - // Note: If you are keeping a local cache of message information - // (e.g. MessageSummary data) for the folder, then you'll need - // to remove the message at e.Index. - messages.RemoveAt (e.Index); + await client.Inbox.OpenAsync (FolderAccess.ReadOnly, cancel.Token); + } + } + + async Task FetchMessageSummariesAsync (bool print) + { + IList fetched = null; + + do { + try { + // fetch summary information for messages that we don't already have + int startIndex = messages.Count; + + fetched = client.Inbox.Fetch (startIndex, -1, request, cancel.Token); + break; + } catch (ImapProtocolException) { + // protocol exceptions often result in the client getting disconnected + await ReconnectAsync (); + } catch (IOException) { + // I/O exceptions always result in the client getting disconnected + await ReconnectAsync (); + } + } while (true); + + foreach (var message in fetched) { + if (print) + Console.WriteLine ("{0}: new message: {1}", client.Inbox, message.Envelope.Subject); + messages.Add (message); + } + } + + async Task WaitForNewMessagesAsync () + { + do { + try { + if (client.Capabilities.HasFlag (ImapCapabilities.Idle)) { + // Note: IMAP servers are only supposed to drop the connection after 30 minutes, so normally + // we'd IDLE for a max of, say, ~29 minutes... but GMail seems to drop idle connections after + // about 10 minutes, so we'll only idle for 9 minutes. + done = new CancellationTokenSource (new TimeSpan (0, 9, 0)); + try { + await client.IdleAsync (done.Token, cancel.Token); + } finally { + done.Dispose (); + done = null; + } } else { - Console.WriteLine ("{0}: expunged message {1}: Unknown message.", folder, e.Index); + // Note: we don't want to spam the IMAP server with NOOP commands, so lets wait a minute + // between each NOOP command. + await Task.Delay (new TimeSpan (0, 1, 0), cancel.Token); + await client.NoOpAsync (cancel.Token); } - }; - - // Keep track of changes to the number of messages in the folder (this is how we'll tell if new messages have arrived). - client.Inbox.CountChanged += (sender, e) => { - // Note: the CountChanged event will fire when new messages arrive in the folder and/or when messages are expunged. - var folder = (ImapFolder) sender; - - Console.WriteLine ("The number of messages in {0} has changed.", folder); - - // Note: because we are keeping track of the MessageExpunged event and updating our - // 'messages' list, we know that if we get a CountChanged event and folder.Count is - // larger than messages.Count, then it means that new messages have arrived. - if (folder.Count > messages.Count) { - Console.WriteLine ("{0} new messages have arrived.", folder.Count - messages.Count); - - // Note: your first instict may be to fetch these new messages now, but you cannot do - // that in an event handler (the ImapFolder is not re-entrant). - // - // If this code had access to the 'done' CancellationTokenSource (see below), it could - // cancel that to cause the IDLE loop to end. + break; + } catch (ImapProtocolException) { + // protocol exceptions often result in the client getting disconnected + await ReconnectAsync (); + } catch (IOException) { + // I/O exceptions always result in the client getting disconnected + await ReconnectAsync (); + } + } while (true); + } + + async Task IdleAsync () + { + do { + try { + await WaitForNewMessagesAsync (); + + if (messagesArrived) { + await FetchMessageSummariesAsync (true); + messagesArrived = false; } - }; + } catch (OperationCanceledException) { + break; + } + } while (!cancel.IsCancellationRequested); + } - // Keep track of flag changes. - client.Inbox.MessageFlagsChanged += (sender, e) => { - var folder = (ImapFolder) sender; + public async Task RunAsync () + { + // connect to the IMAP server and get our initial list of messages + try { + await ReconnectAsync (); + await FetchMessageSummariesAsync (false); + } catch (OperationCanceledException) { + await client.DisconnectAsync (true); + return; + } - Console.WriteLine ("{0}: flags for message {1} have changed to: {2}.", folder, e.Index, e.Flags); - }; + // Note: We capture client.Inbox here because cancelling IdleAsync() *may* require + // disconnecting the IMAP client connection, and, if it does, the `client.Inbox` + // property will no longer be accessible which means we won't be able to disconnect + // our event handlers. + var inbox = client.Inbox; - Console.WriteLine ("Hit any key to end the IDLE loop."); - using (var done = new CancellationTokenSource ()) { - // Note: when the 'done' CancellationTokenSource is cancelled, it ends to IDLE loop. - var thread = new Thread (IdleLoop); + // keep track of changes to the number of messages in the folder (this is how we'll tell if new messages have arrived). + inbox.CountChanged += OnCountChanged; - thread.Start (new IdleState (client, done.Token)); + // keep track of messages being expunged so that when the CountChanged event fires, we can tell if it's + // because new messages have arrived vs messages being removed (or some combination of the two). + inbox.MessageExpunged += OnMessageExpunged; - Console.ReadKey (); - done.Cancel (); - thread.Join (); - } + // keep track of flag changes + inbox.MessageFlagsChanged += OnMessageFlagsChanged; - if (client.Inbox.Count > messages.Count) { - Console.WriteLine ("The new messages that arrived during IDLE are:"); - foreach (var message in client.Inbox.Fetch (messages.Count, -1, MessageSummaryItems.Full | MessageSummaryItems.UniqueId)) - Console.WriteLine ("Subject: {0}", message.Envelope.Subject); - } + await IdleAsync (); - client.Disconnect (true); - } + inbox.MessageFlagsChanged -= OnMessageFlagsChanged; + inbox.MessageExpunged -= OnMessageExpunged; + inbox.CountChanged -= OnCountChanged; + + await client.DisconnectAsync (true); } - class IdleState + // Note: the CountChanged event will fire when new messages arrive in the folder and/or when messages are expunged. + void OnCountChanged (object sender, EventArgs e) { - readonly object mutex = new object (); - CancellationTokenSource timeout; - - /// - /// Get the cancellation token. - /// - /// - /// The cancellation token is the brute-force approach to cancelling the IDLE and/or NOOP command. - /// Using the cancellation token will typically drop the connection to the server and so should - /// not be used unless the client is in the process of shutting down or otherwise needs to - /// immediately abort communication with the server. - /// - /// The cancellation token. - public CancellationToken CancellationToken { get; private set; } - - /// - /// Get the done token. - /// - /// - /// The done token tells the that the user has requested to end the loop. - /// When the done token is cancelled, the will gracefully come to an end by - /// cancelling the timeout and then breaking out of the loop. - /// - /// The done token. - public CancellationToken DoneToken { get; private set; } - - /// - /// Get the IMAP client. - /// - /// The IMAP client. - public ImapClient Client { get; private set; } - - /// - /// Check whether or not either of the CancellationToken's have been cancelled. - /// - /// true if cancellation was requested; otherwise, false. - public bool IsCancellationRequested { - get { - return CancellationToken.IsCancellationRequested || DoneToken.IsCancellationRequested; - } - } + var folder = (ImapFolder) sender; - /// - /// Initializes a new instance of the class. - /// - /// The IMAP client. - /// The user-controlled 'done' token. - /// The brute-force cancellation token. - public IdleState (ImapClient client, CancellationToken doneToken, CancellationToken cancellationToken = default (CancellationToken)) - { - CancellationToken = cancellationToken; - DoneToken = doneToken; - Client = client; - - // When the user hits a key, end the current timeout as well - doneToken.Register (CancelTimeout); - } + // Note: because we are keeping track of the MessageExpunged event and updating our + // 'messages' list, we know that if we get a CountChanged event and folder.Count is + // larger than messages.Count, then it means that new messages have arrived. + if (folder.Count > messages.Count) { + int arrived = folder.Count - messages.Count; - /// - /// Cancel the timeout token source, forcing ImapClient.Idle() to gracefully exit. - /// - void CancelTimeout () - { - lock (mutex) { - if (timeout != null) - timeout.Cancel (); - } - } + if (arrived > 1) + Console.WriteLine ("\t{0} new messages have arrived.", arrived); + else + Console.WriteLine ("\t1 new message has arrived."); - /// - /// Set the timeout source. - /// - /// The timeout source. - public void SetTimeoutSource (CancellationTokenSource source) - { - lock (mutex) { - timeout = source; - - if (timeout != null && IsCancellationRequested) - timeout.Cancel (); - } + // Note: your first instinct may be to fetch these new messages now, but you cannot do + // that in this event handler (the ImapFolder is not re-entrant). + // + // Instead, cancel the `done` token and update our state so that we know new messages + // have arrived. We'll fetch the summaries for these new messages later... + messagesArrived = true; + done?.Cancel (); } } - static void IdleLoop (object state) + void OnMessageExpunged (object sender, MessageEventArgs e) { - var idle = (IdleState) state; + var folder = (ImapFolder) sender; - lock (idle.Client.SyncRoot) { - // Note: since the IMAP server will drop the connection after 30 minutes, we must loop sending IDLE commands that - // last ~29 minutes or until the user has requested that they do not want to IDLE anymore. - // - // For GMail, we use a 9 minute interval because they do not seem to keep the connection alive for more than ~10 minutes. - while (!idle.IsCancellationRequested) { - // Note: Starting with .NET 4.5, you can make this simpler by using the CancellationTokenSource .ctor that - // takes a TimeSpan argument, thus eliminating the need to create a timer. - using (var timeout = new CancellationTokenSource ()) { - using (var timer = new System.Timers.Timer (9 * 60 * 1000)) { - // End the IDLE command when the timer expires. - timer.Elapsed += (sender, e) => timeout.Cancel (); - timer.AutoReset = false; - timer.Enabled = true; - - try { - // We set the timeout source so that if the idle.DoneToken is cancelled, it can cancel the timeout - idle.SetTimeoutSource (timeout); - - if (idle.Client.Capabilities.HasFlag (ImapCapabilities.Idle)) { - // The Idle() method will not return until the timeout has elapsed or idle.CancellationToken is cancelled - idle.Client.Idle (timeout.Token, idle.CancellationToken); - } else { - // The IMAP server does not support IDLE, so send a NOOP command instead - idle.Client.NoOp (idle.CancellationToken); - - // Wait for the timeout to elapse or the cancellation token to be cancelled - WaitHandle.WaitAny (new [] { timeout.Token.WaitHandle, idle.CancellationToken.WaitHandle }); - } - } catch (OperationCanceledException) { - // This means that idle.CancellationToken was cancelled, not the DoneToken nor the timeout. - break; - } catch (ImapProtocolException) { - // The IMAP server sent garbage in a response and the ImapClient was unable to deal with it. - // This should never happen in practice, but it's probably still a good idea to handle it. - // - // Note: an ImapProtocolException almost always results in the ImapClient getting disconnected. - break; - } catch (ImapCommandException) { - // The IMAP server responded with "NO" or "BAD" to either the IDLE command or the NOOP command. - // This should never happen... but again, we're catching it for the sake of completeness. - break; - } finally { - // We're about to Dispose() the timeout source, so set it to null. - idle.SetTimeoutSource (null); - } - } - } - } + if (e.Index < messages.Count) { + var message = messages[e.Index]; + + Console.WriteLine ("{0}: message #{1} has been expunged: {2}", folder, e.Index, message.Envelope.Subject); + + // Note: If you are keeping a local cache of message information + // (e.g. MessageSummary data) for the folder, then you'll need + // to remove the message at e.Index. + messages.RemoveAt (e.Index); + } else { + Console.WriteLine ("{0}: message #{1} has been expunged.", folder, e.Index); } } + + void OnMessageFlagsChanged (object sender, MessageFlagsChangedEventArgs e) + { + var folder = (ImapFolder) sender; + + Console.WriteLine ("{0}: flags have changed for message #{1} ({2}).", folder, e.Index, e.Flags); + } + + public void Exit () + { + cancel.Cancel (); + } + + public void Dispose () + { + client.Dispose (); + cancel.Dispose (); + } } } diff --git a/Documentation/Examples/MessageDeliveryStatusExamples.cs b/Documentation/Examples/MessageDeliveryStatusExamples.cs new file mode 100644 index 0000000000..ead62c867a --- /dev/null +++ b/Documentation/Examples/MessageDeliveryStatusExamples.cs @@ -0,0 +1,86 @@ +// +// MessageDeliveryStatusExamples.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2023 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Linq; + +using MimeKit; + +namespace MimeKit.Examples { + public static class MessageDeliveryStatusExamples + { + #region ProcessDeliveryStatusNotification + public void ProcessDeliveryStatusNotification (MimeMessage message) + { + var report = message.Body as MultipartReport; + + if (report == null || report.ReportType == null || !report.ReportType.Equals ("delivery-status", StringComparison.OrdinalIgnoreCase)) { + // this is not a delivery status notification message... + return; + } + + // process the report + foreach (var mds in report.OfType ()) { + // process the status groups - each status group represents a different recipient + + // The first status group contains information about the message + var envelopeId = mds.StatusGroups[0]["Original-Envelope-Id"]; + + // all of the other status groups contain per-recipient information + for (int i = 1; i < mds.StatusGroups.Length; i++) { + var recipient = mds.StatusGroups[i]["Original-Recipient"]; + var action = mds.StatusGroups[i]["Action"]; + + if (recipient == null) + recipient = mds.StatusGroups[i]["Final-Recipient"]; + + // the recipient string should be in the form: "rfc822;user@domain.com" + var index = recipient.IndexOf (';'); + var address = recipient.Substring (index + 1); + + switch (action) { + case "failed": + Console.WriteLine ("Delivery of message {0} failed for {1}", envelopeId, address); + break; + case "delayed": + Console.WriteLine ("Delivery of message {0} has been delayed for {1}", envelopeId, address); + break; + case "delivered": + Console.WriteLine ("Delivery of message {0} has been delivered to {1}", envelopeId, address); + break; + case "relayed": + Console.WriteLine ("Delivery of message {0} has been relayed for {1}", envelopeId, address); + break; + case "expanded": + Console.WriteLine ("Delivery of message {0} has been delivered to {1} and relayed to the the expanded recipients", envelopeId, address); + break; + } + } + } + } + #endregion + } +} diff --git a/Documentation/Examples/MimeParserExamples.cs b/Documentation/Examples/MimeParserExamples.cs new file mode 100644 index 0000000000..bff03e4bb8 --- /dev/null +++ b/Documentation/Examples/MimeParserExamples.cs @@ -0,0 +1,164 @@ +using System; +using System.IO; +using System.Linq; +using System.Collections.Generic; + +using MimeKit; + +namespace Examples { + class MimeParserExamples + { + #region ParseMessage + public static MimeMessage ParseMessage (string fileName) + { + // Load a MimeMessage from a file path or stream + using (var stream = File.OpenRead (fileName)) { + var parser = new MimeParser (stream, MimeFormat.Entity); + + return parser.ParseMessage (); + } + } + #endregion // ParseMessage + + #region ParseMbox + public static void ParseMbox (string fileName) + { + // Load every message from a Unix mbox spool. + using (var stream = fileName.OpenRead (fileName)) { + var parser = new MimeParser (stream, MimeFormat.Mbox); + + while (!parser.IsEndOfStream) { + MimeMessage message = parser.ParseMessage (); + long mboxMarkerOffset = parser.MboxMarkerOffset; + string mboxMarker = parser.MboxMarker; + + Console.WriteLine ($"MBOX marker found @ {mboxMarkerOffset}: {mboxMarker}"); + + // TODO: Do something with the message. + } + } + } + #endregion // ParseMboxSpool + + #region MessageOffsets + class MimeOffsets + { + public string MimeType { get; set; } + + public long? MboxMarkerOffset { get; set; } + + public int LineNumber { get; set; } + + public long BeginOffset { get; set; } + + public long HeadersEndOffset { get; set; } + + public long EndOffset { get; set; } + + public MimeOffsets Message { get; set; } + + public List Children { get; set; } + + public long Octets { get; set; } + + public int? Lines { get; set; } + } + + public static void MimeOffsetsExample (string fileName) + { + using (var stream = fileName.OpenRead (fileName)) { + var messages = new Dictionary (); + var entities = new Dictionary (); + MimeOffsets messageOffsets = null; + + var parser = new MimeParser (stream, MimeFormat.Entity); + + // Connect a handler to track MimeMessage begin offsets + parser.MimeMessageBegin += delegate (sender, args) { + var parser = (MimeParser) sender; + + // Create a new MimeOffsets for this message. + var offsets = new MimeOffsets { + BeginOffset = args.BeginOffset, + LineNumber = args.LineNumber + }; + + if (args.Parent != null) { + // If we get here, then it means that the MimeMessage is part of + // a message/rfc822 "attachment". + var parentOffsets = entities[args.Parent]; + parentOffsets.Message = offsets; + } else { + // Otherwise, this is the top-level MimeMessage. + offsets.MboxMarkerOffset = parser.MboxMarkerOffset; + messageOffsets = offsets; + } + + messages.Add (args.Message, offsets); + }; + + // Connect a handler to track MimeMessage end offsets + parser.MimeMessageEnd += delegate (sender, args) { + // Our MimeMessageBegin event handler already created a MimeOffsets for + // this message. Use the `messages` dictionary to retrieve it. + var offsets = messages[args.Message]; + + // Track the size of the MimeMessage in octets (aka bytes), the offset + // for the end of the header block, and the end of the message. + offsets.Octets = args.EndOffset - args.HeadersEndOffset; + offsets.HeadersEndOffset = args.HeadersEndOffset; + offsets.EndOffset = args.EndOffset; + }; + + // Connect a handler to track MimeEntity begin offsets + parser.MimeEntityBegin += delegate (sender, args) { + // Create a new MimeOffsets for this MIME entity (which could be a MimePart, MessagePart, or Multipart). + var offsets = new MimeOffsets { + MimeType = args.Entity.ContentType.MimeType, + BeginOffset = args.BeginOffset, + LineNumber = args.LineNumber + }; + + if (args.Parent != null && entities.TryGetValue (args.Parent, out var parentOffsets)) { + parentOffsets.Children ??= new List (); + parentOffsets.Children.Add (offsets); + } + + entities.Add (args.Entity, offsets); + }; + + // Connect a handler to track MimeEntity end offsets + parser.MimeEntityEnd += delegate (sender, args) { + // Our MimeEntityBegin event handler already created a MimeOffsets for + // this entity. Use the `entities` dictionary to retrieve it. + var offsets = entities[args.Entity]; + + // Track the size of the MimeEntity in octets (aka bytes), the offset + // for the end of the header block, the end of the entity, and the + // line count. + offsets.Octets = args.EndOffset - args.HeadersEndOffset; + offsets.HeadersEndOffset = args.HeadersEndOffset; + offsets.EndOffset = args.EndOffset; + offsets.Lines = args.Lines; + }; + + // Parse the message (which will emit the events as appropriate). + var message = parser.ParseMessage (); + + // Now we can find out the offsets of each MimePart: + foreach (var bodyPart in message.BodyParts.OfType ()) { + var offsets = entities[bodyPart]; + + Console.WriteLine ($"The offsets for the MIME part for {bodyPart.ContentType} are:"); + Console.WriteLine ($" - LineNumber: {offsets.LineNumber}") + Console.WriteLine ($" - BeginOffset: {offsets.BeginOffset}"); + Console.WriteLine ($" - HeadersEndOffset: {offsets.HeadersEndOffset}"); // Note: This is also where the *content* begins. + Console.WriteLine ($" - EndOffset: {offsets.BeginOffset}"); + Console.WriteLine ($" - Octets: {offsets.Octets}"); + Console.WriteLine ($" - Lines: {offsets.Lines}"); + } + } + } + #endregion // MessageOffsets + } +} diff --git a/Documentation/Examples/MimeParserMbox.cs b/Documentation/Examples/MimeParserMbox.cs deleted file mode 100644 index f2f09d8f6e..0000000000 --- a/Documentation/Examples/MimeParserMbox.cs +++ /dev/null @@ -1,7 +0,0 @@ -// Load every message from a Unix mbox -var parser = new MimeParser (stream, MimeFormat.Mbox); -while (!parser.IsEndOfStream) { - var message = parser.ParseMessage (); - - // do something with the message -} diff --git a/Documentation/Examples/MimeParserParseMessage.cs b/Documentation/Examples/MimeParserParseMessage.cs deleted file mode 100644 index 20f2ce7fe1..0000000000 --- a/Documentation/Examples/MimeParserParseMessage.cs +++ /dev/null @@ -1,3 +0,0 @@ -// Load a MimeMessage from a stream -var parser = new MimeParser (stream, MimeFormat.Entity); -var message = parser.ParseMessage (); diff --git a/Documentation/Examples/MimeVisitorExamples.cs b/Documentation/Examples/MimeVisitorExamples.cs index 03701b1ce2..23ab6948fc 100644 --- a/Documentation/Examples/MimeVisitorExamples.cs +++ b/Documentation/Examples/MimeVisitorExamples.cs @@ -1,9 +1,9 @@ // // MimeVisitorExamples.cs // -// Author: Jeffrey Stedfast +// Author: Jeffrey Stedfast // -// Copyright (c) 2013-2016 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2023 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -44,16 +44,13 @@ class HtmlPreviewVisitor : MimeVisitor { List stack = new List (); List attachments = new List (); - readonly string tempDir; string body; /// /// Creates a new HtmlPreviewVisitor. /// - /// A temporary directory used for storing image files. - public HtmlPreviewVisitor (string tempDirectory) + public HtmlPreviewVisitor () { - tempDir = tempDirectory; } /// @@ -125,62 +122,71 @@ bool TryGetImage (string url, out MimePart image) return false; } - // Save the image to our temp directory and return a "file://" url suitable for - // the browser control to load. - // Note: if you'd rather embed the image data into the HTML, you can construct a - // "data:" url instead. - string SaveImage (MimePart image, string url) + /// + /// Get a data: URI for the image attachment. + /// + /// + /// Encodes the image attachment into a string suitable for setting as a src= attribute value in + /// an img tag. + /// + /// The data: URI. + /// The image attachment. + string GetDataUri (MimePart image) { - string fileName = url.Replace (':', '_').Replace ('\\', '_').Replace ('/', '_'); - - string path = Path.Combine (tempDir, fileName); + using (var memory = new MemoryStream ()) { + image.Content.DecodeTo (memory); + var buffer = memory.GetBuffer (); + var length = (int) memory.Length; + var base64 = Convert.ToBase64String (buffer, 0, length); - if (!File.Exists (path)) { - using (var output = File.Create (path)) - image.ContentObject.DecodeTo (output); + return string.Format ("data:{0};base64,{1}", image.ContentType.MimeType, base64); } - - return "file://" + path.Replace ('\\', '/'); } // Replaces urls that refer to images embedded within the message with // "file://" urls that the browser control will actually be able to load. void HtmlTagCallback (HtmlTagContext ctx, HtmlWriter htmlWriter) { - if (ctx.TagId == HtmlTagId.Image && !ctx.IsEndTag && stack.Count > 0) { + if (ctx.TagId == HtmlTagId.Meta && !ctx.IsEndTag) { + bool isContentType = false; + ctx.WriteTag (htmlWriter, false); - // replace the src attribute with a file:// URL + // replace charsets with "utf-8" since our output will be in utf-8 (and not whatever the original charset was) foreach (var attribute in ctx.Attributes) { - if (attribute.Id == HtmlAttributeId.Src) { - MimePart image; - string url; + if (attribute.Id == HtmlAttributeId.Charset) { + htmlWriter.WriteAttributeName (attribute.Name); + htmlWriter.WriteAttributeValue ("utf-8"); + } else if (isContentType && attribute.Id == HtmlAttributeId.Content) { + htmlWriter.WriteAttributeName (attribute.Name); + htmlWriter.WriteAttributeValue ("text/html; charset=utf-8"); + } else { + if (attribute.Id == HtmlAttributeId.HttpEquiv && attribute.Value != null + && attribute.Value.Equals ("Content-Type", StringComparison.OrdinalIgnoreCase)) + isContentType = true; + + htmlWriter.WriteAttribute (attribute); + } + } + } else if (ctx.TagId == HtmlTagId.Image && !ctx.IsEndTag && stack.Count > 0) { + ctx.WriteTag (htmlWriter, false); - if (!TryGetImage (attribute.Value, out image)) { + // replace the src attribute with a "data:" URL + foreach (var attribute in ctx.Attributes) { + if (attribute.Id == HtmlAttributeId.Src) { + if (!TryGetImage (attribute.Value, out var image)) { htmlWriter.WriteAttribute (attribute); continue; } - url = SaveImage (image, attribute.Value); + var dataUri = GetDataUri (image); htmlWriter.WriteAttributeName (attribute.Name); - htmlWriter.WriteAttributeValue (url); + htmlWriter.WriteAttributeValue (dataUri); } else { htmlWriter.WriteAttribute (attribute); } } - } else if (ctx.TagId == HtmlTagId.Body && !ctx.IsEndTag) { - ctx.WriteTag (htmlWriter, false); - - // add and/or replace oncontextmenu="return false;" - foreach (var attribute in ctx.Attributes) { - if (attribute.Name.ToLowerInvariant () == "oncontextmenu") - continue; - - htmlWriter.WriteAttribute (attribute); - } - - htmlWriter.WriteAttribute ("oncontextmenu", "return false;"); } else { // pass the tag through to the output ctx.WriteTag (htmlWriter, true); @@ -206,7 +212,7 @@ protected override void VisitTextPart (TextPart entity) string delsp; if (entity.ContentType.Parameters.TryGetValue ("delsp", out delsp)) - flowed.DeleteSpace = delsp.ToLowerInvariant () == "yes"; + flowed.DeleteSpace = delsp.Equals ("yes", StringComparison.OrdinalIgnoreCase); converter = flowed; } else { diff --git a/Documentation/Examples/MultipartFormDataExample.cs b/Documentation/Examples/MultipartFormDataExample.cs deleted file mode 100644 index 722f490e73..0000000000 --- a/Documentation/Examples/MultipartFormDataExample.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.Net; - -using MimeKit; - -namespace Examples { - class MultipartFormDataExample - { - #region ParseMultipartFormDataSimple - MimeEntity ParseMultipartFormData (HttpWebResponse response) - { - var contentType = ContentType.Parse (response.ContentType); - - return MimeEntity.Parse (contentType, response.GetResponseStream ()); - } - #endregion - - #region ParseMultipartFormDataComplex - MimeEntity ParseMultipartFormData (HttpWebResponse response) - { - // create a temporary file to store our large HTTP data stream - var tmp = Path.GetTempFileName (); - - using (var stream = File.Open (tmp, FileMode.Open, FileAccess.ReadWrite)) { - // create a header for the multipart/form-data MIME entity based on the Content-Type value of the HTTP - // response - var header = Encoding.UTF8.GetBytes (string.Format ("Content-Type: {0}\r\n\r\n", response.ContentType)); - - // write the header to the stream - stream.Write (header, 0, header.Length); - - // copy the content of the HTTP response to our temporary stream - response.GetResponseStream ().CopyTo (stream); - - // reset the stream back to the beginning - stream.Position = 0; - - // parse the MIME entity with persistent = true, telling the parser not to load the content into memory - return MimeEntity.Load (stream, persistent: true); - } - } - #endregion - } -} diff --git a/Documentation/Examples/MultipartFormDataExamples.cs b/Documentation/Examples/MultipartFormDataExamples.cs index 722f490e73..157af96124 100644 --- a/Documentation/Examples/MultipartFormDataExamples.cs +++ b/Documentation/Examples/MultipartFormDataExamples.cs @@ -11,7 +11,7 @@ MimeEntity ParseMultipartFormData (HttpWebResponse response) { var contentType = ContentType.Parse (response.ContentType); - return MimeEntity.Parse (contentType, response.GetResponseStream ()); + return MimeEntity.Load (contentType, response.GetResponseStream ()); } #endregion diff --git a/Documentation/Examples/OAuth2ExchangeExample.cs b/Documentation/Examples/OAuth2ExchangeExample.cs new file mode 100644 index 0000000000..8e3be0b6c4 --- /dev/null +++ b/Documentation/Examples/OAuth2ExchangeExample.cs @@ -0,0 +1,68 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +using MailKit; +using MailKit.Net.Imap; +using MailKit.Security; + +using Microsoft.Identity.Client; + +namespace OAuth2ExchangeExample { + class Program + { + const string ExchangeAccount = "username@office365.com"; + + public static void Main (string[] args) + { + using (var client = new ImapClient ()) { + client.Connect ("outlook.office365.com", 993, SecureSocketOptions.SslOnConnect); + if (client.AuthenticationMechanisms.Contains ("OAUTHBEARER") || client.AuthenticationMechanisms.Contains ("XOAUTH2")) + AuthenticateAsync (client).GetAwaiter ().GetResult (); + client.Disconnect (true); + } + } + + static async Task AuthenticateAsync (ImapClient client) + { + var options = new PublicClientApplicationOptions { + ClientId = "Application (client) ID", + TenantId = "Directory (tenant) ID", + RedirectUri = "https://login.microsoftonline.com/common/oauth2/nativeclient" + }; + + var publicClientApplication = PublicClientApplicationBuilder + .CreateWithApplicationOptions (options) + .Build (); + + var scopes = new string[] { + "email", + "offline_access", + "https://outlook.office.com/IMAP.AccessAsUser.All", // Only needed for IMAP + //"https://outlook.office.com/POP.AccessAsUser.All", // Only needed for POP + //"https://outlook.office.com/SMTP.AccessAsUser.All", // Only needed for SMTP + }; + + AuthenticationResult? result; + + try { + // First, check the cache for an auth token. + result = await publicClientApplication.AcquireTokenSilent (scopes, username).ExecuteAsync (); + } catch (MsalUiRequiredException) { + // If that fails, then try getting an auth token interactively. + result = await publicClientApplication.AcquireTokenInteractive (scopes).WithLoginHint (username).ExecuteAsync (); + } + + // Note: We use result.Account.Username here instead of ExchangeAccount because the user *may* have chosen a + // different Microsoft Exchange account when presented with the browser window during the authentication process. + SaslMechanism oauth2; + + if (client.AuthenticationMechanisms.Contains ("OAUTHBEARER")) + oauth2 = new SaslMechanismOAuthBearer (result.Account.Username, result.AccessToken); + else + oauth2 = new SaslMechanismOAuth2 (result.Account.Username, result.AccessToken); + + await client.AuthenticateAsync (oauth2); + } + } +} diff --git a/Documentation/Examples/OAuth2GMailExample.cs b/Documentation/Examples/OAuth2GMailExample.cs new file mode 100644 index 0000000000..95ad1dc03a --- /dev/null +++ b/Documentation/Examples/OAuth2GMailExample.cs @@ -0,0 +1,63 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +using Google.Apis.Util; +using Google.Apis.Util.Store; +using Google.Apis.Auth.OAuth2; +using Google.Apis.Auth.OAuth2.Flows; + +using MailKit; +using MailKit.Net.Imap; +using MailKit.Security; + +namespace OAuth2GMailExample { + class Program + { + const string GMailAccount = "username@gmail.com"; + + public static void Main (string[] args) + { + using (var client = new ImapClient ()) { + client.Connect ("imap.gmail.com", 993, SecureSocketOptions.SslOnConnect); + if (client.AuthenticationMechanisms.Contains ("OAUTHBEARER") || client.AuthenticationMechanisms.Contains ("XOAUTH2")) + AuthenticateAsync (client).GetAwaiter ().GetResult (); + client.Disconnect (true); + } + } + + static async Task AuthenticateAsync (ImapClient client) + { + var clientSecrets = new ClientSecrets { + ClientId = "XXX.apps.googleusercontent.com", + ClientSecret = "XXX" + }; + + var codeFlow = new GoogleAuthorizationCodeFlow (new GoogleAuthorizationCodeFlow.Initializer { + DataStore = new FileDataStore ("CredentialCacheFolder", false), + Scopes = new [] { "https://mail.google.com/" }, + ClientSecrets = clientSecrets + }); + + // Note: For a web app, you'll want to use AuthorizationCodeWebApp instead. + var codeReceiver = new LocalServerCodeReceiver (); + var authCode = new AuthorizationCodeInstalledApp (codeFlow, codeReceiver); + + var credential = await authCode.AuthorizeAsync (GMailAccount, CancellationToken.None); + + if (credential.Token.IsStale) + await credential.RefreshTokenAsync (CancellationToken.None); + + // Note: We use credential.UserId here instead of GMailAccount because the user *may* have chosen a + // different GMail account when presented with the browser window during the authentication process. + SaslMechanism oauth2; + + if (client.AuthenticationMechanisms.Contains ("OAUTHBEARER")) + oauth2 = new SaslMechanismOAuthBearer (credential.UserId, credential.Token.AccessToken); + else + oauth2 = new SaslMechanismOAuth2 (credential.UserId, credential.Token.AccessToken); + + await client.AuthenticateAsync (oauth2); + } + } +} diff --git a/Documentation/Examples/OpenPGPExamples.cs b/Documentation/Examples/OpenPGPExamples.cs index 6007cce257..b22d054aaf 100644 --- a/Documentation/Examples/OpenPGPExamples.cs +++ b/Documentation/Examples/OpenPGPExamples.cs @@ -126,9 +126,14 @@ static Stream Decrypt (MimeMessage message) { var text = message.TextBody; - using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var encrypted = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { using (var ctx = new MyGnuPGContext ()) { - return ctx.GetDecryptedStream (memory); + var decrypted = new MemoryStream (); + + ctx.DecryptTo (encrypted, decrypted); + decrypted.Position = 0; + + return decrypted; } } } diff --git a/Documentation/Examples/OpeningContent.cs b/Documentation/Examples/OpeningContent.cs index a52334c927..18e9dde1b5 100644 --- a/Documentation/Examples/OpeningContent.cs +++ b/Documentation/Examples/OpeningContent.cs @@ -1,4 +1,4 @@ -using (var stream = part.ContentObject.Open ()) { +using (var stream = part.Content.Open ()) { // At this point, you can now read from the stream as if it were the original, // raw content. Assuming you have an image UI control that could load from a // stream, you could do something like this: diff --git a/Documentation/Examples/ParameterExamples.cs b/Documentation/Examples/ParameterExamples.cs new file mode 100644 index 0000000000..fa35bc8cd0 --- /dev/null +++ b/Documentation/Examples/ParameterExamples.cs @@ -0,0 +1,27 @@ +using System; + +using MimeKit; + +namespace MimeKit.Examples +{ + public static class ParameterExamples + { + public void OverrideAllParameterEncodings (MimePart part) + { + #region OverrideAllParameterEncodings + // Some versions of Outlook expect the rfc2047 style of encoding of parameter values. + foreach (var parameter in part.ContentDisposition.Parameters) + parameter.EncodingMethod = ParameterEncodingMethod.Rfc2047; + #endregion OverrideAllParameterEncodings + } + + public void OverrideFileNameParameterEncodings (MimePart part) + { + #region OverrideFileNameParameterEncoding + // Some versions of Outlook expect the rfc2047 style of encoding for the filename parameter value. + if (part.ContentDisposition.Parameters.TryGetValue ("filename", out var parameter)) + parameter.EncodingMethod = ParameterEncodingMethod.Rfc2047; + #endregion OverrideFileNameParameterEncoding + } + } +} diff --git a/Documentation/Examples/Pop3Examples.cs b/Documentation/Examples/Pop3Examples.cs index 7594e8fb68..9acb31a703 100644 --- a/Documentation/Examples/Pop3Examples.cs +++ b/Documentation/Examples/Pop3Examples.cs @@ -1,9 +1,9 @@ // // Pop3Examples.cs // -// Author: Jeffrey Stedfast +// Author: Jeffrey Stedfast // -// Copyright (c) 2013-2016 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2023 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -68,9 +68,6 @@ public static void PrintCapabilities () if (client.Capabilities.HasFlag (Pop3Capabilities.SASL)) { var mechanisms = string.Join (", ", client.AuthenticationMechanisms); Console.WriteLine ("The POP3 server supports the following SASL mechanisms: {0}", mechanisms); - - // Note: if we don't want MailKit to use a particular SASL mechanism, we can disable it like this: - client.AuthenticationMechanisms.Remove ("XOAUTH2"); } client.Authenticate ("username", "password"); @@ -267,5 +264,36 @@ public static void DownloadNewMessages (HashSet previouslyDownloadedUids } } #endregion + + #region SslConnectionInformation + public static void PrintSslConnectionInfo (string host, int port) + { + using (var client = new SmtpClient ()) { + client.Connect (host, port, SecureSocketOptions.Auto); + + Console.WriteLine ($"Negotiated the following SSL options with {host}:"); + Console.WriteLine ($" Protocol Version: {client.SslProtocol}"); + Console.WriteLine ($" Cipher Algorithm: {client.SslCipherAlgorithm}"); + Console.WriteLine ($" Cipher Strength: {client.SslCipherStrength}"); + Console.WriteLine ($" Hash Algorithm: {client.SslHashAlgorithm}"); + Console.WriteLine ($" Hash Strength: {client.SslHashStrength}"); + Console.WriteLine ($" Key-Exchange Algorithm: {client.SslKeyExchangeAlgorithm}"); + Console.WriteLine ($" Key-Exchange Strength: {client.SslKeyExchangeStrength}"); + + // Example Log: + // + // Negotiated the following SSL options with pop.gmail.com: + // Protocol Version: Tls12 + // Cipher Algorithm: Aes128 + // Cipher Strength: 128 + // Hash Algorithm: Sha256 + // Hash Strength: 0 + // Key-Exchange Algorithm: 44550 + // Key-Exchange Strength: 255 + + client.Disconnect (true); + } + } + #endregion } } diff --git a/Documentation/Examples/InvalidSslCertificate.cs b/Documentation/Examples/ProxyExamples.cs similarity index 74% rename from Documentation/Examples/InvalidSslCertificate.cs rename to Documentation/Examples/ProxyExamples.cs index aa7cf74b7b..adc917c2e3 100644 --- a/Documentation/Examples/InvalidSslCertificate.cs +++ b/Documentation/Examples/ProxyExamples.cs @@ -1,9 +1,9 @@ -// -// SmtpExamples.cs // -// Author: Jeffrey Stedfast +// ProxyExamples.cs // -// Copyright (c) 2013-2016 Xamarin Inc. (www.xamarin.com) +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2023 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -31,26 +31,26 @@ using MailKit; using MailKit.Security; using MailKit.Net.Smtp; +using MailKit.Net.Pop3; +using MailKit.Net.Imap; +using MailKit.Net.Proxy; -namespace MailKit.Examples -{ - public static class SslCertificateValidationExamples +namespace MailKit.Examples { + public static class ProxyExamples { - public static void SendMessage (MimeMessage message) + #region ProxyClient + public static void SendMessageViaProxy (MimeMessage message) { - #region Simple using (var client = new SmtpClient ()) { - client.ServerCertificateValidationCallback = (s, c, h, e) => true; - + client.ProxyClient = new Socks5Proxy ("socks5.proxy.com", 1080, new NetworkCredentials ("proxyUserName", "proxyPassword")); client.Connect ("smtp.gmail.com", 465, SecureSocketOptions.SslOnConnect); - client.Authenticate ("username", "password"); client.Send (message); client.Disconnect (true); } - #endregion Simple } + #endregion } } diff --git a/Documentation/Examples/SMimeExamples.cs b/Documentation/Examples/SMimeExamples.cs index ac6431b310..fa5de663e7 100644 --- a/Documentation/Examples/SMimeExamples.cs +++ b/Documentation/Examples/SMimeExamples.cs @@ -39,7 +39,7 @@ public void RegisterMySecureMimeContext () #region RegisterCustomContext // Note: by registering our custom context it becomes the default S/MIME context // instantiated by MimeKit when methods such as Encrypt(), Decrypt(), Sign(), and - // Verify() are used without an expliit context. + // Verify() are used without an explicit context. CryptographyContext.Register (typeof (MySecureMimeContext)); #endregion } diff --git a/Documentation/Examples/SmtpExamples.cs b/Documentation/Examples/SmtpExamples.cs index fe37d65c52..af99028989 100644 --- a/Documentation/Examples/SmtpExamples.cs +++ b/Documentation/Examples/SmtpExamples.cs @@ -1,9 +1,9 @@ // // SmtpExamples.cs // -// Author: Jeffrey Stedfast +// Author: Jeffrey Stedfast // -// Copyright (c) 2013-2016 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2023 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -35,6 +35,80 @@ namespace MailKit.Examples { public static class SmtpExamples { + #region SaveToPickupDirectory + public static void SaveToPickupDirectory (MimeMessage message, string pickupDirectory) + { + do { + // Generate a random file name to save the message to. + var path = Path.Combine (pickupDirectory, Guid.NewGuid ().ToString () + ".eml"); + Stream stream; + + try { + // Attempt to create the new file. + stream = File.Open (path, FileMode.CreateNew); + } catch (IOException) { + // If the file already exists, try again with a new Guid. + if (File.Exists (path)) + continue; + + // Otherwise, fail immediately since it probably means that there is + // no graceful way to recover from this error. + throw; + } + + try { + using (stream) { + // IIS pickup directories expect the message to be "byte-stuffed" + // which means that lines beginning with "." need to be escaped + // by adding an extra "." to the beginning of the line. + // + // Use an SmtpDataFilter to "byte-stuff" the message as it is written + // to the file stream. This is the same process that an SmtpClient + // would use when sending the message in a `DATA` command. + using (var filtered = new FilteredStream (stream)) { + filtered.Add (new SmtpDataFilter ()); + + // Make sure to write the message in DOS () format. + var options = FormatOptions.Default.Clone (); + options.NewLineFormat = NewLineFormat.Dos; + + message.WriteTo (options, filtered); + filtered.Flush (); + return; + } + } + } catch { + // An exception here probably means that the disk is full. + // + // Delete the file that was created above so that incomplete files are not + // left behind for IIS to send accidentally. + File.Delete (path); + throw; + } + } while (true); + } + #endregion + + #region LoadFromPickupDirectory + public static MimeMessage LoadFromPickupDirectory (string fileName) + { + using (var stream = File.OpenRead (fileName)) { + // IIS pickup directories store messages that have been "byte-stuffed" + // which means that lines beginning with "." have been escaped by + // adding an extra "." to the beginning of the line. + // + // Use an SmtpDataFilter to decode the message as it is loaded from + // the file stream. This is the reverse process that an SmtpClient + // would use when sending the message in a `DATA` command. + using (var filtered = new FilteredStream (stream)) { + filtered.Add (new SmtpDataFilter (decode: true)); + + return MimeMessage.Load (filtered); + } + } + } + #endregion + #region ProtocolLogger public static void SendMessage (MimeMessage message) { @@ -47,6 +121,52 @@ public static void SendMessage (MimeMessage message) client.Disconnect (true); } + + // Example log: + // + // Connected to smtps://smtp.gmail.com:465/ + // S: 220 smtp.gmail.com ESMTP w81sm22057166qkg.43 - gsmtp + // C: EHLO [192.168.1.220] + // S: 250-smtp.gmail.com at your service, [192.168.1.220] + // S: 250-SIZE 35882577 + // S: 250-8BITMIME + // S: 250-AUTH LOGIN PLAIN XOAUTH2 PLAIN-CLIENTTOKEN OAUTHBEARER XOAUTH + // S: 250-ENHANCEDSTATUSCODES + // S: 250-PIPELINING + // S: 250-CHUNKING + // S: 250 SMTPUTF8 + // C: AUTH PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk + // S: 235 2.7.0 Accepted + // C: MAIL FROM: + // C: RCPT TO: + // S: 250 2.1.0 OK w81sm22057166qkg.43 - gsmtp + // S: 250 2.1.5 OK w81sm22057166qkg.43 - gsmtp + // C: DATA + // S: 354 Go ahead w81sm22057166qkg.43 - gsmtp + // C: From: "LastName, FirstName" + // C: Date: Thu, 27 Dec 2018 10:55:18 -0500 + // C: Subject: This is a test message + // C: Message-Id: + // C: To: "LastName, FirstName" + // C: MIME-Version: 1.0 + // C: Content-Type: multipart/alternative; boundary="=-CToJI+AD2gS6z+fFlzDvhg==" + // C: + // C: --=-CToJI+AD2gS6z+fFlzDvhg== + // C: Content-Type: text/plain; charset=utf-8 + // C: Content-Transfer-Encoding: quoted-printable + // C: + // C: This is the text/plain message body. + // C: --=-CToJI+AD2gS6z+fFlzDvhg== + // C: Content-Type: text/html; charset=utf-8 + // C: Content-Transfer-Encoding: quoted-printable + // C: + // C:
This is the text/html message body.
+ // C: --=-CToJI+AD2gS6z+fFlzDvhg==-- + // C: + // C: . + // S: 250 2.0.0 OK 1545926120 w81sm22057166qkg.43 - gsmtp + // C: QUIT + // S: 221 2.0.0 closing connection w81sm22057166qkg.43 - gsmtp } #endregion @@ -59,10 +179,6 @@ public static void PrintCapabilities () if (client.Capabilities.HasFlag (SmtpCapabilities.Authentication)) { var mechanisms = string.Join (", ", client.AuthenticationMechanisms); Console.WriteLine ("The SMTP server supports the following SASL mechanisms: {0}", mechanisms); - - // Note: if we don't want MailKit to use a particular SASL mechanism, we can disable it like this: - client.AuthenticationMechanisms.Remove ("XOAUTH2"); - client.Authenticate ("username", "password"); } @@ -215,6 +331,43 @@ public static void SendMessages (IList messages) } #endregion + #region VerifyAddress + public static void VerifyAddress () + { + using (var client = new SmtpClient ()) { + client.Connect ("smtp.mail-server.com", 465, SecureSocketOptions.SslOnConnect); + client.Authenticate ("username", "password"); + + try { + var verified = client.Verify ("smith"); + Console.WriteLine ($"'smith' was resolved the the following mailbox: {verified}"); + } catch (SmtpCommandException ex) { + Console.WriteLine ($"'smith' is not a valid address: {ex.Message}"); + } + + client.Disconnect (true); + } + } + #endregion + + #region ExpandAlias + public static void ExpandAlias (string alias) + { + using (var client = new SmtpClient ()) { + client.Connect ("smtp.mail-server.com", 465, SecureSocketOptions.SslOnConnect); + client.Authenticate ("username", "password"); + + var expanded = client.Expand (alias); + + Console.WriteLine ($"Expanding the alias '{alias}' results in the following list of addresses:"); + foreach (var mailbox in expanded.Mailboxes) + Console.WriteLine ($"* {mailbox}"); + + client.Disconnect (true); + } + } + #endregion + #region DeliveryStatusNotification public class DSNSmtpClient : SmtpClient { @@ -259,5 +412,36 @@ protected override string GetEnvelopeId (MimeMessage message) } } #endregion + + #region SslConnectionInformation + public static void PrintSslConnectionInfo (string host, int port) + { + using (var client = new SmtpClient ()) { + client.Connect (host, port, SecureSocketOptions.Auto); + + Console.WriteLine ($"Negotiated the following SSL options with {host}:"); + Console.WriteLine ($" Protocol Version: {client.SslProtocol}"); + Console.WriteLine ($" Cipher Algorithm: {client.SslCipherAlgorithm}"); + Console.WriteLine ($" Cipher Strength: {client.SslCipherStrength}"); + Console.WriteLine ($" Hash Algorithm: {client.SslHashAlgorithm}"); + Console.WriteLine ($" Hash Strength: {client.SslHashStrength}"); + Console.WriteLine ($" Key-Exchange Algorithm: {client.SslKeyExchangeAlgorithm}"); + Console.WriteLine ($" Key-Exchange Strength: {client.SslKeyExchangeStrength}"); + + // Example Log: + // + // Negotiated the following SSL options with smtp.gmail.com: + // Protocol Version: Tls12 + // Cipher Algorithm: Aes128 + // Cipher Strength: 128 + // Hash Algorithm: Sha256 + // Hash Strength: 0 + // Key-Exchange Algorithm: 44550 + // Key-Exchange Strength: 255 + + client.Disconnect (true); + } + } + #endregion } } diff --git a/Documentation/Examples/SslCertificateValidation.cs b/Documentation/Examples/SslCertificateValidation.cs new file mode 100644 index 0000000000..a861b84a1e --- /dev/null +++ b/Documentation/Examples/SslCertificateValidation.cs @@ -0,0 +1,81 @@ +using System; +using System.Net.Security; +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; + +using MimeKit; +using MailKit; +using MailKit.Security; +using MailKit.Net.Smtp; + +namespace MailKit.Examples +{ + public static class SslCertificateValidationExample + { + public static void SendMessage (MimeMessage message) + { + using (var client = new SmtpClient ()) { + // Set our custom SSL certificate validation callback. + client.ServerCertificateValidationCallback = MySslCertificateValidationCallback; + + // Connect to smtp.gmail.com on the SSL-wrapped port. + client.Connect ("smtp.gmail.com", 465, SecureSocketOptions.SslOnConnect); + + // Authenticate with our username and password. + client.Authenticate ("username@gmail.com", "password"); + + // Send our message. + client.Send (message); + + // Disconnect cleanly from the server. + client.Disconnect (true); + } + } + + static bool MySslCertificateValidationCallback (object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) + { + // If there are no errors, then everything went smoothly. + if (sslPolicyErrors == SslPolicyErrors.None) + return true; + + // Note: MailKit will always pass the host name string as the `sender` argument. + var host = (string) sender; + + if ((sslPolicyErrors & SslPolicyErrors.RemoteCertificateNotAvailable) != 0) { + // This means that the remote certificate is unavailable. Notify the user and return false. + Console.WriteLine ("The SSL certificate was not available for {0}", host); + return false; + } + + if ((sslPolicyErrors & SslPolicyErrors.RemoteCertificateNameMismatch) != 0) { + // This means that the server's SSL certificate did not match the host name that we are trying to connect to. + var certificate2 = certificate as X509Certificate2; + var cn = certificate2 != null ? certificate2.GetNameInfo (X509NameType.SimpleName, false) : certificate.Subject; + + Console.WriteLine ("The Common Name for the SSL certificate did not match {0}. Instead, it was {1}.", host, cn); + return false; + } + + // The only other errors left are chain errors. + Console.WriteLine ("The SSL certificate for the server could not be validated for the following reasons:"); + + // The first element's certificate will be the server's SSL certificate (and will match the `certificate` argument) + // while the last element in the chain will typically either be the Root Certificate Authority's certificate -or- it + // will be a non-authoritative self-signed certificate that the server admin created. + foreach (var element in chain.ChainElements) { + // Each element in the chain will have its own status list. If the status list is empty, it means that the + // certificate itself did not contain any errors. + if (element.ChainElementStatus.Length == 0) + continue; + + Console.WriteLine ("\u2022 {0}", element.Certificate.Subject); + foreach (var error in element.ChainElementStatus) { + // `error.StatusInformation` contains a human-readable error string while `error.Status` is the corresponding enum value. + Console.WriteLine ("\t\u2022 {0}", error.StatusInformation); + } + } + + return false; + } + } +} diff --git a/Documentation/media/gmail-imap-pop3-settings.png b/Documentation/media/gmail-imap-pop3-settings.png new file mode 100644 index 0000000000..ab8a1bfa00 Binary files /dev/null and b/Documentation/media/gmail-imap-pop3-settings.png differ diff --git a/Documentation/media/google-developer-console/click-create-credentials.png b/Documentation/media/google-developer-console/click-create-credentials.png new file mode 100644 index 0000000000..df9315463a Binary files /dev/null and b/Documentation/media/google-developer-console/click-create-credentials.png differ diff --git a/Documentation/media/google-developer-console/click-new-project.png b/Documentation/media/google-developer-console/click-new-project.png new file mode 100644 index 0000000000..7719840456 Binary files /dev/null and b/Documentation/media/google-developer-console/click-new-project.png differ diff --git a/Documentation/media/google-developer-console/click-oauth-consent-screen-menu.png b/Documentation/media/google-developer-console/click-oauth-consent-screen-menu.png new file mode 100644 index 0000000000..8ffb6a1af3 Binary files /dev/null and b/Documentation/media/google-developer-console/click-oauth-consent-screen-menu.png differ diff --git a/Documentation/media/google-developer-console/click-select-a-project.png b/Documentation/media/google-developer-console/click-select-a-project.png new file mode 100644 index 0000000000..a9d781ef9a Binary files /dev/null and b/Documentation/media/google-developer-console/click-select-a-project.png differ diff --git a/Documentation/media/google-developer-console/client-id-and-secret.png b/Documentation/media/google-developer-console/client-id-and-secret.png new file mode 100644 index 0000000000..2dcd9d1722 Binary files /dev/null and b/Documentation/media/google-developer-console/client-id-and-secret.png differ diff --git a/Documentation/media/google-developer-console/create-new-project.png b/Documentation/media/google-developer-console/create-new-project.png new file mode 100644 index 0000000000..eb97cb67bd Binary files /dev/null and b/Documentation/media/google-developer-console/create-new-project.png differ diff --git a/Documentation/media/google-developer-console/oauth-consent-screen.png b/Documentation/media/google-developer-console/oauth-consent-screen.png new file mode 100644 index 0000000000..4c9e087b92 Binary files /dev/null and b/Documentation/media/google-developer-console/oauth-consent-screen.png differ diff --git a/Documentation/media/google-developer-console/select-application-type-other.png b/Documentation/media/google-developer-console/select-application-type-other.png new file mode 100644 index 0000000000..21bd05b0a4 Binary files /dev/null and b/Documentation/media/google-developer-console/select-application-type-other.png differ diff --git a/Documentation/media/google-developer-console/select-external.png b/Documentation/media/google-developer-console/select-external.png new file mode 100644 index 0000000000..1fe01dce54 Binary files /dev/null and b/Documentation/media/google-developer-console/select-external.png differ diff --git a/ExchangeOAuth2.md b/ExchangeOAuth2.md new file mode 100644 index 0000000000..83b5d7d42d --- /dev/null +++ b/ExchangeOAuth2.md @@ -0,0 +1,358 @@ +# Using OAuth2 With Exchange (IMAP, POP3 or SMTP) + +## Quick Index + +* [Registering Your Application with Microsoft](#registering-your-application-with-microsoft) +* [Configuring the Correct API Permissions for Your Application](#configuring-the-correct-api-permissions-for-your-application) +* Desktop and Mobile Applications + * [Authenticating a Desktop or Mobile Application with OAuth2](#authenticating-a-desktop-or-mobile-application-with-oauth2) +* Web Applications + * [Authenticating a Web Application with OAuth2](#authenticating-a-web-application-with-oauth2) +* Web Services + * [Registering Service Principals for Your Web Service](#registering-service-principals-for-your-web-service) + * [Granting Permissions for Your Web Service](#granting-permissions-for-your-web-service) + * [Authenticating a Web Service with OAuth2](#authenticating-a-web-service-with-oauth2) +* [Additional Resources](#additional-resources) + +## Registering Your Application with Microsoft + +Whether you are writing a Desktop, Mobile or Web Service application, the first thing you'll need to do is register your +application with Microsoft's Identity Platform. To do this, go to Microsoft's +[Quickstart guide](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app) +and follow the instructions. + +## Configuring the Correct API Permissions for Your Application + +There are several different API permissions that you may want to configure depending on which protocols your application intends to use. + +Follow the instructions for [adding the POP, IMAP, and/or SMTP permissions to your Entra AD application](https://learn.microsoft.com/en-us/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth#use-client-credentials-grant-flow-to-authenticate-smtp-imap-and-pop-connections). + +## Desktop and Mobile Applications + +### Authenticating a Desktop or Mobile Application with OAuth2 + +Now that you have the **Client ID** and **Tenant ID** strings, you'll need to plug those values into +your application. + +The following sample code uses the [Microsoft.Identity.Client](https://www.nuget.org/packages/Microsoft.Identity.Client/) +nuget package for obtaining the access token which will be needed by MailKit to pass on to the Exchange +server. + +```csharp +static async Task GetPublicClientOAuth2CredentialsAsync (string protocol, string emailAddress, CancellationToken cancellationToken = default) +{ + var options = new PublicClientApplicationOptions { + ClientId = "Application (client) ID", + TenantId = "Directory (tenant) ID", + + // Use "https://login.microsoftonline.com/common/oauth2/nativeclient" for apps using + // embedded browsers or "http://localhost" for apps that use system browsers. + RedirectUri = "https://login.microsoftonline.com/common/oauth2/nativeclient" + }; + + var publicClientApplication = PublicClientApplicationBuilder + .CreateWithApplicationOptions (options) + .Build (); + + string[] scopes; + + if (protocol.Equals ("IMAP", StringComparison.OrdinalIgnoreCase)) { + scopes = new string[] { + "email", + "offline_access", + "https://outlook.office.com/IMAP.AccessAsUser.All" + }; + } else if (protocol.Equals ("POP", StringComparison.OrdinalIgnoreCase)) { + scopes = new string[] { + "email", + "offline_access", + "https://outlook.office.com/POP.AccessAsUser.All" + }; + } else { + scopes = new string[] { + "email", + "offline_access", + "https://outlook.office.com/SMTP.Send" + }; + } + + try { + // First, check the cache for an auth token. + return await publicClientApplication.AcquireTokenSilent (scopes, emailAddress).ExecuteAsync (cancellationToken); + } catch (MsalUiRequiredException) { + // If that fails, then try getting an auth token interactively. + return await publicClientApplication.AcquireTokenInteractive (scopes).WithLoginHint (emailAddress).ExecuteAsync (cancellationToken); + } +} +``` + +#### IMAP (using PublicClientApplication) + +```csharp +var result = await GetPublicClientOAuth2CredentialsAsync ("IMAP", "username@outlook.com"); + +// Note: We always use result.Account.Username instead of `Username` because the user may have selected an alternative account. +var oauth2 = new SaslMechanismOAuth2 (result.Account.Username, result.AccessToken); + +using (var client = new ImapClient ()) { + await client.ConnectAsync ("outlook.office365.com", 993, SecureSocketOptions.SslOnConnect); + await client.AuthenticateAsync (oauth2); + await client.DisconnectAsync (true); +} +``` + +#### SMTP (using PublicClientApplication) + +```csharp +var result = await GetPublicClientOAuth2CredentialsAsync ("SMTP", "username@outlook.com"); + +// Note: We always use result.Account.Username instead of `Username` because the user may have selected an alternative account. +var oauth2 = new SaslMechanismOAuth2 (result.Account.Username, result.AccessToken); + +using (var client = new SmtpClient ()) { + await client.ConnectAsync ("smtp.office365.com", 587, SecureSocketOptions.StartTls); + await client.AuthenticateAsync (oauth2); + await client.DisconnectAsync (true); +} +``` + +Note: Once you've acquired an auth token using the interactive method above, you can avoid prompting the user +if you cache the `result.Account` information and then silently reacquire auth tokens in the future using +the following code: + +```csharp +var result = await publicClientApplication.AcquireTokenSilent(scopes, account).ExecuteAsync(cancellationToken); +``` + +Note: for information on caching tokens, see Microsoft's documentation about how to implement a +[cross-platform token cache](https://github.com/AzureAD/microsoft-authentication-extensions-for-dotnet/wiki/Cross-platform-Token-Cache). + +## Web Applications + +### Authenticating a Web Application with OAuth2 + +Use this if you want to send/receive mail on behalf of a user. + +```csharp +// Common Code +using Microsoft.Graph; +using Microsoft.Identity.Client; +using Microsoft.Kiota.Abstractions.Authentication; + +public static class OAuthMicrosoft +{ + public static readonly string[] RegistrationScopes = new string[] { + "offline_access", + "User.Read", + "Mail.Send", + "https://outlook.office.com/SMTP.Send", + "https://outlook.office.com/IMAP.AccessAsUser.All", + }; + + public static readonly string[] SmtpScopes = new string[] { + "email", + "offline_access", + "https://outlook.office.com/SMTP.Send" + }; + + public static readonly string[] ImapScopes = new string[] { + "email", + "offline_access", + "https://outlook.office.com/IMAP.AccessAsUser.All", + }; + + public static IConfidentialClientApplication CreateConfidentialClient () + { + var clientId = "Application (client) ID"; + var tenantId = "common"; // common = anybody with microsoft account personal or organization; other options see https://learn.microsoft.com/en-us/entra/identity-platform/v2-protocols#endpoints + var clientSecret = "client secret"; + + var redirectURL = "https://example.com/oauth/microsoft/callback"; + + var confidentialClientApplication = ConfidentialClientApplicationBuilder.Create (clientId) + .WithAuthority ($"https://login.microsoftonline.com/{tenantId}/v2.0") + .WithClientSecret (clientSecret) + .WithRedirectUri (redirectURL) + .Build (); + + // You also need to configure an MSAL token cache. so that token are remembered. + return confidentialClientApplication; + } +} +``` + +```csharp +// Registration page - redirect user to Microsoft to get authorization +public async Task OnPostAsync () +{ + var client = OAuthMicrosoft.CreateConfidentialClient (); + + // Note: When getting authorization, specify all of the scopes that your application will ever need (eg. SMTP /and/ IMAP). + // Later, when requesting an access token, you will only ask for the specific scopes that you need (e.g. SMTP). + var authurlbuilder = client.GetAuthorizationRequestUrl (OAuthMicrosoft.RegistrationScopes); + var authurl = await authurlbuilder.ExecuteAsync (); + + return this.Redirect (authurl.ToString ()); +} + +// Callback page = https://example.com/oauth/microsoft/callback in this example +public async Task OnGet ([FromQuery] string code) +{ + var confidentialClientApplication = OAuthMicrosoft.CreateConfidentialClient (); + var scopes = OAuthMicrosoft.SmtpScopes; + + var auth = await confidentialClientApplication.AcquireTokenByAuthorizationCode (scopes, code).ExecuteAsync (); //this saves the token in msal cache + + var ident = auth.Account.HomeAccountId.Identifier; + // Note: you will need to persist the ident to refer to later. +} + +// Use the credentials + +public async Task SendEmailAsync (string ident) +{ + var confidentialClientApplication = OAuthMicrosoft.CreateConfidentialClient (); + var account = await confidentialClientApplication.GetAccountAsync (ident); + var scopes = OAuthMicrosoft.SmtpScopes; + + try { + var auth = await confidentialClientApplication.AcquireTokenSilent (scopes, account).ExecuteAsync (); + + using (var client = new SmtpClient ()) { + await client.ConnectAsync ("smtp-mail.outlook.com", 587, SecureSocketOptions.StartTls); + + var oauth2 = new SaslMechanismOAuth2 (auth.Account.Username, auth.AccessToken); + + await client.AuthenticateAsync (oauth2); + + var serverfeedback = await client.SendAsync (message); + await client.DisconnectAsync (true); + } + } catch (MsalUiRequiredException) { + throw new Exception ("Need to get authorization again"); + } +} + +public async Task TestImapAsync (string ident) +{ + var confidentialClientApplication = OAuthMicrosoft.CreateConfidentialClient (); + var account = await confidentialClientApplication.GetAccountAsync (ident); + var scopes = OAuthMicrosoft.ImapScopes; + + var auth = await confidentialClientApplication.AcquireTokenSilent (scopes, account).ExecuteAsync (); + + var oauth2 = new SaslMechanismOAuth2 (auth.Account.Username, auth.AccessToken); + + using (var client = new ImapClient ()) { + await client.ConnectAsync ("outlook.office365.com", 993, SecureSocketOptions.SslOnConnect); + await client.AuthenticateAsync (oauth2); + await client.DisconnectAsync (true); + } +} +``` + +## Web Services + +### Registering Service Principals for Your Web Service + +Once your web service has been registered, the tenant admin will need to register your service principal. + +To use the New-ServicePrincipal cmdlet, open an [Azure Powershell](https://learn.microsoft.com/en-us/powershell/azure/new-azureps-module-az?view=azps-10.2.0) +terminal and install ExchangeOnlineManagement and connect to your tenant as shown below: + +```powershell +Install-Module -Name ExchangeOnlineManagement -allowprerelease +Import-module ExchangeOnlineManagement +Connect-ExchangeOnline -Organization +``` + +Next, register the Service Principal for your web service: + +```powershell +New-ServicePrincipal -AppId -ObjectId [-Organization ] +``` + +Note: In the Azure portal, make sure you retrieve the Object ID from the Service Principal, under Enterprise Applications, and not the App Registration. + +### Granting Permissions for Your Web Service + +In order to grant permissions for your web service to access an Office365 and/or Exchange account, you'll need to first get the +Service Principal ID registered in the previous step using the following command: + +```powershell +Get-ServicePrincipal | fl +``` + +Once you have the Service Principal ID for your web service, use the following command to add full +mailbox permissions for the email account that your web service will be accessing: + +```powershelllo;.k,; +Add-MailboxPermission -Identity "john.smith@example.com" -User + -AccessRights FullAccess +``` + +### Authenticating a Web Service with OAuth2 + +Now that you have the **Client ID** and **Tenant ID** strings, you'll need to plug those values into +your application. + +The following sample code uses the [Microsoft.Identity.Client](https://www.nuget.org/packages/Microsoft.Identity.Client/) +nuget package for obtaining the access token which will be needed by MailKit to pass on to the Exchange +server. + +```csharp +static async Task GetConfidentialClientOAuth2CredentialsAsync (string protocol, CancellationToken cancellationToken = default) +{ + var confidentialClientApplication = ConfidentialClientApplicationBuilder.Create (clientId) + .WithAuthority ($"https://login.microsoftonline.com/{tenantId}/v2.0") + .WithCertificate (certificate) // or .WithClientSecret (clientSecret) + .Build (); + + string[] scopes; + + if (protocol.Equals ("SMTP", StringComparison.OrdinalIgnoreCase)) { + scopes = new string[] { + // For SMTP, use the following scope + "https://outlook.office365.com/.default" + }; + } else { + scopes = new string[] { + // For IMAP and POP3, use the following scope + "https://ps.outlook.com/.default" + }; + } + + return await confidentialClientApplication.AcquireTokenForClient (scopes).ExecuteAsync (cancellationToken); +} +``` + +#### IMAP (using ConfidentialClientApplication) + +```csharp +var result = await GetConfidentialClientOAuth2CredentialsAsync ("IMAP"); +var oauth2 = new SaslMechanismOAuth2 ("username@outlook.com", result.AccessToken); + +using (var client = new ImapClient ()) { + await client.ConnectAsync ("outlook.office365.com", 993, SecureSocketOptions.SslOnConnect); + await client.AuthenticateAsync (oauth2); + await client.DisconnectAsync (true); +} +``` + +#### SMTP (using ConfidentialClientApplication) + +```csharp +var result = await GetConfidentialClientOAuth2CredentialsAsync ("SMTP"); +var oauth2 = new SaslMechanismOAuth2 ("username@outlook.com", result.AccessToken); + +using (var client = new SmtpClient ()) { + await client.ConnectAsync ("smtp.office365.com", 587, SecureSocketOptions.StartTls); + await client.AuthenticateAsync (oauth2); + await client.DisconnectAsync (true); +} +``` + +## Additional Resources + +For more information, check out the [Microsoft.Identity.Client](https://docs.microsoft.com/en-us/dotnet/api/microsoft.identity.client?view=azure-dotnet) +documentation. diff --git a/FAQ.md b/FAQ.md index 3e123ff2e7..8cab1e3346 100644 --- a/FAQ.md +++ b/FAQ.md @@ -3,64 +3,173 @@ ## Question Index ### General -* [Are MimeKit and MailKit completely free? Can I use them in my proprietary product(s)?](#CompletelyFree) -* [Why do I get `The remote certificate is invalid according to the validation procedure` when I try to Connect?](#InvalidSslCertificate) -* [How can I get a protocol log for IMAP, POP3, or SMTP to see what is going wrong?](#ProtocolLog) -* [Why doesn't MailKit find some of my GMail POP3 or IMAP messages?](#GMailHiddenMessages) -* [How can I access GMail using MailKit?](#GMailAccess) -* [How can I log in to a GMail account using OAuth 2.0?](#GMailOAuth2) + +* [Are MimeKit and MailKit completely free? Can I use them in my proprietary product(s)?](#completely-free) +* [Why do I get `NotSupportedException: No data is available for encoding ######. For information on defining a custom encoding, see the documentation for the Encoding.RegisterProvider method.`?](#register-provider) +* [Why does text show up garbled in my ASP.NET Core / .NET Core / .NET 5+ app?](#garbled-text) +* [Why do I get a `TypeLoadException` when I try to create a new MimeMessage?](#type-load-exception) +* [Why do I get `"MailKit.Security.SslHandshakeException: An error occurred while attempting to establish an SSL or TLS connection."` when I try to Connect?](#ssl-handshake-exception) +* [How can I get a protocol log for IMAP, POP3, or SMTP to see what is going wrong?](#protocol-log) +* [Why doesn't MailKit find some of my GMail POP3 or IMAP messages?](#gmail-hidden-messages) +* [How can I access GMail using MailKit?](#gmail-access) +* [How can I log in to a GMail account using OAuth 2.0?](#gmail-oauth2) ### Messages -* [How can I create a message with attachments?](#CreateAttachments) -* [How can I get the main body of a message?](#MessageBody) -* [How can I tell if a message has attachments?](#HasAttachments) -* [Why doesn't the `MimeMessage` class implement `ISerializable` so that I can serialize a message to disk and read it back later?](#Serialize) -* [How can I parse messages?](#LoadMessages) -* [How can I save messages?](#SaveMessages) -* [How can I save attachments?](#SaveAttachments) -* [How can I get the email addresses in the From, To, and Cc headers?](#AddressHeaders) -* [Why do attachments with unicode filenames appear as "ATT0####.dat" in Outlook?](#UntitledAttachments) -* [How can I decrypt PGP messages that are embedded in the main message text?](#DecryptInlinePGP) -* [How can I reply to a message?](#Reply) -* [How can I forward a message?](#Forward) + +* [How can I create a message with attachments?](#create-attachments) +* [How can I get the main body of a message?](#message-body) +* [How can I tell if a message has attachments?](#has-attachments) +* [Why doesn't the `MimeMessage` class implement `ISerializable` so that I can serialize a message to disk and read it back later?](#serialize-message) +* [How can I parse messages?](#load-messages) +* [How can I save messages?](#save-messages) +* [How can I save attachments?](#save-attachments) +* [How can I get the email addresses in the From, To, and Cc headers?](#address-headers) +* [Why do attachments with Unicode filenames appear as "ATT0####.dat" in Outlook?](#untitled-attachments) +* [How can I decrypt PGP messages that are embedded in the main message text?](#decrypt-inline-pgp) +* [How can I reply to a message?](#reply-message) +* [How can I forward a message?](#forward-message) ### ImapClient -* [How can I get the number of unread messages in a folder?](#ImapUnreadCount) -* [How can I search for messages delivered between two dates?](#ImapSearchBetween2Dates) -* [What does "The ImapClient is currently busy processing a command." mean?](#ImapClientBusy) -* [Why do I get InvalidOperationException: "The folder is not currently open."?](#FolderNotOpenException) -* [Why doesn't ImapFolder.MoveTo() move the message out of the source folder?](#ImapMoveDoesNotMove) -* [How can I mark messages as read using IMAP?](#ImapMarkAsRead) + +* [How can I get the number of unread messages in a folder?](#imap-unread-count) +* [How can I search for messages delivered between two dates?](#imap-search-date-range) +* [What does "The ImapClient is currently busy processing a command." mean?](#imap-client-busy) +* [Why do I get InvalidOperationException: "The folder is not currently open."?](#imap-folder-not-open-exception) +* [Why doesn't ImapFolder.MoveTo() move the message out of the source folder?](#imap-move-does-not-move) +* [How can I mark messages as read using IMAP?](#imap-mark-as-read) +* [How can I re-synchronize the cache for an IMAP folder?](#imap-folder-resync) +* [How can I login using a shared mailbox in Office365?](#office365-shared-mailboxes) ### SmtpClient -* [How can I send email to the SpecifiedPickupDirectory?](#SpecifiedPickupDirectory) -* [How can I request a notification when the message is read by the user?](#SmtpRequestReadReceipt) -* [How can I process a read receipt notification?](#SmtpProcessReadReceipt) +* [Why doesn't the message show up in the "Sent Mail" folder after sending it?](#smtp-sent-folder) +* [How can I send email to the SpecifiedPickupDirectory?](#smtp-specified-pickup-directory) +* [How can I request a notification when the message is read by the user?](#smtp-request-read-receipt) +* [How can I process a read receipt notification?](#smtp-process-read-receipt) ## General -### Q: Are MimeKit and MailKit completely free? Can I use them in my proprietary product(s)? +### Q: Are MimeKit and MailKit completely free? Can I use them in my proprietary product(s)? Yes. MimeKit and MailKit are both completely free and open source. They are both covered under the [MIT](https://opensource.org/licenses/MIT) license. -### Q: Why do I get `The remote certificate is invalid according to the validation procedure` when I try to Connect? +### Q: Why do I get `NotSupportedException: No data is available for encoding ######. For information on defining a custom encoding, see the documentation for the Encoding.RegisterProvider method.`? + +In .NET Core, Microsoft decided to split out the non-Unicode text encodings into a separate NuGet package called +[System.Text.Encoding.CodePages](https://www.nuget.org/packages/System.Text.Encoding.CodePages). + +MimeKit already pulls in a reference to this NuGet package, so you shouldn't need to add a reference to it in +your project. That said, you will still need to register the encoding provider. It is recommended that you add +the following line of code to your program initialization (e.g. the beginning of your program's Main() method): + +```csharp +System.Text.Encoding.RegisterProvider (System.Text.CodePagesEncodingProvider.Instance); +``` + +### Q: Why does text show up garbled in my ASP.NET Core / .NET Core / .NET 5+ app? + +.NET Core (and ASP.NET Core by extension) and .NET 5 (and later) only provide the Unicode encodings, ASCII and ISO-8859-1 by default. +Other text encodings are not available to your application unless your application +[registers](https://docs.microsoft.com/en-us/dotnet/api/system.text.encoding.registerprovider?view=net-5.0) the encoding +provider that provides all of the additional encodings. + +First, add a package reference for the [System.Text.Encoding.CodePages](https://www.nuget.org/packages/System.Text.Encoding.CodePages) +nuget package to your project and then register the additional text encodings using the following code snippet: + +```csharp +System.Text.Encoding.RegisterProvider (System.Text.CodePagesEncodingProvider.Instance); +``` + +Note: The above code snippet should be safe to call in .NET Framework versions >= 4.6 as well. + +### Q: Why do I get a `TypeLoadException` when I try to create a new MimeMessage? + +This only seems to happen in cases where the application is built for .NET Framework (v4.x) and seems to be most +common for ASP.NET web applications that were built using Visual Studio 2019 (it is unclear whether this happens +with Visual Studio 2022 as well). + +The issue is that some (older?) versions of MSBuild do not correctly generate `\*.dll.config`, `app.config` +and/or `web.config` files with proper assembly version binding redirects. + +If this problem is happening to you, make sure to use MimeKit and MailKit >= v4.0 which include `MimeKit.dll.config` +and `MailKit.dll.config`. + +The next step is to manually edit your application's `app.config` (or `web.config`) to add a binding redirect +for `System.Runtime.CompilerServices.Unsafe`: + +```xml + + + + + + + + + + +``` + +### Q: Why do I get `"MailKit.Security.SslHandshakeException: An error occurred while attempting to establish an SSL or TLS connection."` when I try to Connect? + +When you get an exception with that error message, it usually means that you are encountering +one of the following scenarios: + +#### 1. The mail server does not support SSL on the specified port. + +There are 2 different ways to use SSL/TLS encryption with mail servers. + +The first way is to enable SSL/TLS encryption immediately upon connecting to the +SMTP, POP3 or IMAP server. This method requires an "SSL port" because the standard +port defined for the protocol is meant for plain-text communication. + +The second way is via a `STARTTLS` command (aka `STLS` for POP3) that is *optionally* +supported by the server. + +Below is a table of the protocols supported by MailKit and the standard plain-text ports +(which either do not support any SSL/TLS encryption at all or only via the `STARTTLS` +command extension) and the SSL ports which require SSL/TLS encryption immediately upon a +successful connection to the remote host. + +|Protocol|Standard Port|SSL Port| +|:------:|:-----------:|:------:| +| SMTP | 25 or 587 | 465 | +| POP3 | 110 | 995 | +| IMAP | 143 | 993 | + +It is important to use the correct `SecureSocketOptions` for the port that you are connecting to. + +If you are connecting to one of the standard ports above, you will need to use `SecureSocketOptions.None`, +`SecureSocketOptions.StartTls` or `SecureSocketOptions.StartTlsWhenAvailable`. -When you get an exception with that error message, it means that the IMAP, POP3 or SMTP -server that you are connecting to is using an SSL certificate that is either expired -or untrusted by your system. +If you are connecting to one of the SSL ports, you will need to use `SecureSocketOptions.SslOnConnect`. -Often times, mail servers will use self-signed certificates instead of using a certificate -that has been signed by a trusted Certificate Authority. When your system is unable to -validate the mail server's certificate because it is not signed by a known and trusted -Certificate Authority, the above error will occur. +You could also try using `SecureSocketOptions.Auto` which works by choosing the appropriate option to use +by comparing the specified port to the ports in the above table. -You can work around this problem by supplying a custom [RemoteServerCertificateValidationCallback](https://msdn.microsoft.com/en-us/library/ms145054) -and setting it on the client's [ServerCertificateValidationCallback](http://mimekit.net/docs/html/P_MailKit_MailService_ServerCertificateValidationCallback.htm) +#### 2. The mail server that you are connecting to is using an expired (or otherwise untrusted) SSL certificate. + +Often times, mail servers will use self-signed certificates instead of using a certificate that +has been signed by a trusted Certificate Authority. Another potential pitfall is when locally +installed anti-virus software replaces the certificate in order to scan web traffic for viruses. + +When your system is unable to validate the mail server's certificate because it is not signed +by a known and trusted Certificate Authority, the above error will occur. + +If you are on a Linux system or are running a web service in a Linux container, it might be possible to use the following command to install +the standard set of Certificate Authority root certificates using the following command: + +```text +apt update && apt install -y ca-certificates +``` + +Another option is to work around this problem by supplying a custom [RemoteCertificateValidationCallback](https://msdn.microsoft.com/en-us/library/ms145054) +and setting it on the client's [ServerCertificateValidationCallback](https://mimekit.net/docs/html/P_MailKit_MailService_ServerCertificateValidationCallback.htm) property. -In the most simplest example, you could do something like this (although I would strongly recommend against it in production use): +In the simplest example, you could do something like this (although I would strongly recommend against it in +production use): ```csharp using (var client = new SmtpClient ()) { @@ -72,18 +181,99 @@ using (var client = new SmtpClient ()) { } ``` -Most likely you'll want to instead compare the certificate's [Thumbprint](https://msdn.microsoft.com/en-us/library/system.security.cryptography.x509certificates.x509certificate2.thumbprint(v=vs.110).aspx) -property to a known value that you have verified at a prior date. +A better solution might be to compare the certificate's common name, issuer, serial number, and fingerprint +to known values to make sure that the certificate can be trusted. Take the following code snippet as an +example of how to do this: + +```csharp +bool MyServerCertificateValidationCallback (object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) +{ + if (sslPolicyErrors == SslPolicyErrors.None) + return true; + + // Note: The following code casts to an X509Certificate2 because it's easier to get the + // values for comparison, but it's possible to get them from an X509Certificate as well. + if (certificate is X509Certificate2 certificate2) { + var cn = certificate2.GetNameInfo (X509NameType.SimpleName, false); + var fingerprint = certificate2.Thumbprint; + var serial = certificate2.SerialNumber; + var issuer = certificate2.Issuer; + + return cn == "imap.gmail.com" && issuer == "CN=GTS CA 1O1, O=Google Trust Services, C=US" && + serial == "00A15434C2695FB1880300000000CBF786" && + fingerprint == "F351BCB631771F19AF41DFF22EB0A0839092DA51"; + } + + return false; +} +``` + +The downside of the above example is that it requires hard-coding known values for "trusted" mail server +certificates which can quickly become unwieldy to deal with if your program is meant to be used with +a wide range of mail servers. + +The best approach would be to prompt the user with a dialog explaining that the certificate is +not trusted for the reasons enumerated by the +[SslPolicyErrors](https://docs.microsoft.com/en-us/dotnet/api/system.net.security.sslpolicyerrors?view=netframework-4.8) +argument as well as potentially the errors provided in the +[X509Chain](https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.x509certificates.x509chain?view=netframework-4.8). +If the user wishes to accept the risks of trusting the certificate, your program could then `return true`. + +For more details on writing a custom SSL certificate validation callback, it may be worth checking out the +[SslCertificateValidation.cs](https://github.com/jstedfast/MailKit/blob/master/Documentation/Examples/SslCertificateValidation.cs) +example. + +#### 3. A Certificate Authority CRL server for one or more of the certificates in the chain is temporarily unavailable. + +Most Certificate Authorities are probably pretty good at keeping their CRL and/or OCSP servers up 24/7, but occasionally +they *do* go down or are otherwise unreachable due to other network problems between you and the server. When this happens, +it becomes impossible to check the revocation status of one or more of the certificates in the chain. + +To ignore revocation checks, you can set the +[CheckCertificateRevocation](https://www.mimekit.net/docs/html/P_MailKit_IMailService_CheckCertificateRevocation.htm) +property of the IMAP, POP3 or SMTP client to `false` before you connect: + +```csharp +using (var client = new SmtpClient ()) { + client.CheckCertificateRevocation = false; + + client.Connect (hostName, port, SecureSocketOptions.Auto); + + // ... +} +``` + +#### 4. The server does not support the same set of SSL/TLS protocols that the client is configured to use. + +MailKit attempts to keep up with the latest security recommendations and so is continuously removing older SSL and TLS +protocols that are no longer considered secure from the default configuration. This often means that MailKit's SMTP, +POP3 and IMAP clients will fail to connect to servers that are still using older SSL and TLS protocols. Currently, +the SSL and TLS protocols that are not supported by default are: SSL v2.0, SSL v3.0, TLS v1.0 and TLS v1.1. + +You can override MailKit's default set of supported +[SSL and TLS protocols](https://docs.microsoft.com/en-us/dotnet/api/system.security.authentication.sslprotocols?view=netframework-4.8) +by setting the value of the [SslProtocols](https://www.mimekit.net/docs/html/P_MailKit_MailService_SslProtocols.htm) +property on your SMTP, POP3 or IMAP client. -You could also use this callback to prompt the user (much like you have probably seen web browsers do) -as to whether or not certificate should be trusted. +For example: + +```csharp +using (var client = new SmtpClient ()) { + // Allow SSLv3.0 and all versions of TLS + client.SslProtocols = SslProtocols.Ssl3 | SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12 | SslProtocols.Tls13; -### Q: How can I get a protocol log for IMAP, POP3, or SMTP to see what is going wrong? + client.Connect ("smtp.gmail.com", 465, true); + + // ... +} +``` + +### Q: How can I get a protocol log for IMAP, POP3, or SMTP to see what is going wrong? All of MailKit's client implementations have a constructor that takes a nifty -[IProtocolLogger](http://www.mimekit.net/docs/html/T_MailKit_IProtocolLogger.htm) +[IProtocolLogger](https://www.mimekit.net/docs/html/T_MailKit_IProtocolLogger.htm) interface for logging client/server communications. Out of the box, you can use the -handy [ProtocolLogger](http://www.mimekit.net/docs/html/T_MailKit_ProtocolLogger.htm) class. +handy [ProtocolLogger](https://www.mimekit.net/docs/html/T_MailKit_ProtocolLogger.htm) class. Here are some examples of how to use it: ```csharp @@ -102,7 +292,7 @@ encoded blob immediately following an `AUTHENTICATE` or `AUTH` command (dependin The only exception to this case is if you are authenticating with `NTLM` in which case I *may* need this information, but *only if* the bug/error is in the authentication step. -### Q: Why doesn't MailKit find some of my GMail POP3 or IMAP messages? +### Q: Why doesn't MailKit find some of my GMail POP3 or IMAP messages? By default, GMail's POP3 and IMAP server does not behave like standard POP3 or IMAP servers and hides messages from clients using those protocols (as well as having other non-standard @@ -113,29 +303,54 @@ intended to behave according to their protocol specifications, you'll need to lo GMail account via your web browser and navigate to the `Forwarding and POP/IMAP` tab of your GMail Settings page and set your options to look like this: -![GMail POP3 and IMAP Settings](http://content.screencast.com/users/jeff.xamarin/folders/Jing/media/7d50dada-6cb0-4ab1-b117-8600fb5e07d4/00000022.png "GMail POP3 and IMAP Settings") +![GMail POP3 and IMAP Settings](Documentation/media/gmail-imap-pop3-settings.png "GMail POP3 and IMAP Settings") + +#### POP download: + +**1. Status:** POP is enabled for all mail that has arrived since 12/31/69 +- [X] Enable POP for **all mail** (even mail that's already been downloaded) +- [ ] Enable POP for **mail that arrives from now on** +- [ ] **Disable** POP -### Q: How can I access GMail using MailKit? +**2. When messages are accessed with POP** \[keep GMail's copy in the Inbox] -The first thing that you will need to do is to configure your GMail account to -[enable less secure apps](https://www.google.com/settings/security/lesssecureapps), -or you'll need to use [OAuth 2.0 authentication](#GMailOAuth2) (which is a bit more complex). +#### IMAP access: + +**When I mark a message in IMAP as deleted:** +- [ ] Auto-Expunge on - Immediately update the server. (default) +- [X] Auto-Expunge off - Wait for the client to update the server. + +**When a message is marked as deleted and expunged from the last visible IMAP folder:** +- [ ] Archive the message (default) +- [ ] Move the message to the Trash +- [X] Immediately delete the message forever + +**Folder size limits** +- [X] Do not limit the number of messages in an IMAP folder (default) +- [ ] Limit IMAP folders to contain no more than this many messages \[1000] + +### Q: How can I access GMail using MailKit? + +As of September 30th, 2024, authentication using only a username and password is [no longer supported by Google](https://support.google.com/accounts/answer/6010255?hl=en). + +There are now only 2 options to choose from: + +1. Use [OAuth 2.0 authentication](#gmail-oauth2) +2. Use an "App password" + +To use an App password, you will first need to [turn on 2-Step Verification](https://support.google.com/accounts/answer/185839). +Once 2-Step Verification is turned on, you can [generate an App password](https://myaccount.google.com/apppasswords). Then, assuming that your GMail account is `user@gmail.com`, you would use the following code snippet to connect to GMail via IMAP: ```csharp using (var client = new ImapClient ()) { - client.ServerCertificateValidationCallback = (s,c,ch,e) => true; client.Connect ("imap.gmail.com", 993, SecureSocketOptions.SslOnConnect); - - // disable OAuth2 authentication unless you are actually using an access_token - client.AuthenticationMechanisms.Remove ("XOAUTH2"); - - client.Authenticate ("user@gmail.com", "password"); - + client.Authenticate ("user@gmail.com", "app-specific-password"); + // do stuff... - + client.Disconnect (true); } ``` @@ -143,52 +358,63 @@ using (var client = new ImapClient ()) { Connecting via POP3 or SMTP is identical except for the host names and ports (and, of course, you'd use a `Pop3Client` or `SmtpClient` as appropriate). -### Q: How can I log in to a GMail account using OAuth 2.0? +### Q: How can I log in to a GMail account using OAuth 2.0? The first thing you need to do is follow -[Google's instructions](https://developers.google.com/accounts/docs/OAuth2) +[Google's instructions](https://developers.google.com/accounts/docs/OAuth2) for obtaining OAuth 2.0 credentials for your application. -Once you've done that, the easiest way to obtain an access token is to use Google's +(Or, as an alternative set of step-by-step instructions, you can follow the directions that I have +written in [GMailOAuth2.md](https://github.com/jstedfast/MailKit/blob/master/GMailOAuth2.md).) + +Once you've done that, the easiest way to obtain an access token is to use Google's [Google.Apis.Auth](https://www.nuget.org/packages/Google.Apis.Auth/) library: ```csharp -var certificate = new X509Certificate2 (@"C:\path\to\certificate.p12", "password", X509KeyStorageFlags.Exportable); -var credential = new ServiceAccountCredential (new ServiceAccountCredential - .Initializer ("your-developer-id@developer.gserviceaccount.com") { - // Note: other scopes can be found here: https://developers.google.com/gmail/api/auth/scopes - Scopes = new[] { "https://mail.google.com/" }, - User = "username@gmail.com" -}.FromCertificate (certificate)); +const string GMailAccount = "username@gmail.com"; -bool result = await credential.RequestAccessTokenAsync (CancellationToken.None); +var clientSecrets = new ClientSecrets { + ClientId = "XXX.apps.googleusercontent.com", + ClientSecret = "XXX" +}; -// Note: result will be true if the access token was received successfully -``` +var codeFlow = new GoogleAuthorizationCodeFlow (new GoogleAuthorizationCodeFlow.Initializer { + // Cache tokens in ~/.local/share/google-filedatastore/CredentialCacheFolder on Linux/Mac + DataStore = new FileDataStore ("CredentialCacheFolder", false), + Scopes = new [] { "https://mail.google.com/" }, + ClientSecrets = clientSecrets, + LoginHint = GMailAccount +}); -Now that you have an access token (`credential.Token.AccessToken`), you can use it with MailKit as if it were -the password: +// Note: For a web app, you'll want to use AuthorizationCodeWebApp instead. +var codeReceiver = new LocalServerCodeReceiver (); +var authCode = new AuthorizationCodeInstalledApp (codeFlow, codeReceiver); + +var credential = await authCode.AuthorizeAsync (GMailAccount, CancellationToken.None); + +if (credential.Token.IsStale) + await credential.RefreshTokenAsync (CancellationToken.None); + +var oauth2 = new SaslMechanismOAuthBearer (credential.UserId, credential.Token.AccessToken); -```csharp using (var client = new ImapClient ()) { - client.Connect ("imap.gmail.com", 993, true); - - // use the access token as the password string - client.Authenticate ("username@gmail.com", credential.Token.AccessToken); + await client.ConnectAsync ("imap.gmail.com", 993, SecureSocketOptions.SslOnConnect); + await client.AuthenticateAsync (oauth2); + await client.DisconnectAsync (true); } ``` ## Messages -### Q: How can I create a message with attachments? +### Q: How can I create a message with attachments? To construct a message with attachments, the first thing you'll need to do is create a `multipart/mixed` container which you'll then want to add the message body to first. Once you've added the body, you can then add MIME parts to it that contain the content of the files you'd like to attach, being sure to set the `Content-Disposition` header value to attachment. You'll probably also want to set the `filename` parameter on the `Content-Disposition` header as well as the `name` parameter on the `Content-Type` -header. The most convenient way to do this is to simply use the -[MimePart.FileName](http://www.mimekit.net/docs/html/P_MimeKit_MimePart_FileName.htm) property which +header. The most convenient way to do this is to use the +[MimePart.FileName](https://www.mimekit.net/docs/html/P_MimeKit_MimePart_FileName.htm) property which will set both parameters for you as well as setting the `Content-Disposition` header value to `attachment` if it has not already been set to something else. @@ -213,7 +439,7 @@ Will you be my +1? // create an image attachment for the file located at path var attachment = new MimePart ("image", "gif") { - ContentObject = new ContentObject (File.OpenRead (path), ContentEncoding.Default), + Content = new MimeContent (File.OpenRead (path), ContentEncoding.Default), ContentDisposition = new ContentDisposition (ContentDisposition.Attachment), ContentTransferEncoding = ContentEncoding.Base64, FileName = Path.GetFileName (path) @@ -230,7 +456,7 @@ message.Body = multipart; ``` A simpler way to construct messages with attachments is to take advantage of the -[BodyBuilder](http://www.mimekit.net/docs/html/T_MimeKit_BodyBuilder.htm) class. +[BodyBuilder](https://www.mimekit.net/docs/html/T_MimeKit_BodyBuilder.htm) class. ```csharp var message = new MimeMessage (); @@ -258,11 +484,11 @@ builder.Attachments.Add (@"C:\Users\Joey\Documents\party.ics"); message.Body = builder.ToMessageBody (); ``` -For more information, see [Creating Messages](http://www.mimekit.net/docs/html/CreatingMessages.htm). +For more information, see [Creating Messages](https://www.mimekit.net/docs/html/Creating-Messages.htm). -### Q: How can I get the main body of a message? +### Q: How can I get the main body of a message? -(Note: for the TL;DR version, skip to [the end](#MessageBodyTLDR)) +(Note: for the TL;DR version, skip to [the end](#message-body-tldr)) MIME is a tree structure of parts. There are multiparts which contain other parts (even other multiparts). There are message parts which contain messages. And finally, there are leaf-node parts which contain content. @@ -324,20 +550,20 @@ There are a few common message structures: application/zip ``` -Now, if you don't care about any of that and just want to get the text of +Now, if you don't care about any of that and just want to get the text of the first `text/plain` or `text/html` part you can find, that's easy. -[MimeMessage](http://www.mimekit.net/docs/html/T_MimeKit_MimeMessage.htm) has two convenience properties -for this: [TextBody](http://www.mimekit.net/docs/html/P_MimeKit_MimeMessage_TextBody.htm) and -[HtmlBody](http://www.mimekit.net/docs/html/P_MimeKit_MimeMessage_HtmlBody.htm). +[MimeMessage](https://www.mimekit.net/docs/html/T_MimeKit_MimeMessage.htm) has two convenience properties +for this: [TextBody](https://www.mimekit.net/docs/html/P_MimeKit_MimeMessage_TextBody.htm) and +[HtmlBody](https://www.mimekit.net/docs/html/P_MimeKit_MimeMessage_HtmlBody.htm). `MimeMessage.HtmlBody`, as the name implies, will traverse the MIME structure for you and find the most appropriate body part with a `Content-Type` of `text/html` that can be interpreted as the message body. Likewise, the `TextBody` property can be used to get the `text/plain` version of the message body. -For more information, see [Working with Messages](http://www.mimekit.net/docs/html/WorkingWithMessages.htm). +For more information, see [Working with Messages](https://www.mimekit.net/docs/html/Working-With-Messages.htm). -### Q: How can I tell if a message has attachments? +### Q: How can I tell if a message has attachments? In most cases, a message with a body that has a MIME-type of `multipart/mixed` containing more than a single part probably has attachments. As illustrated above, the first part of a `multipart/mixed` is @@ -345,7 +571,7 @@ typically the textual body of the message, but it is not always quite that simpl In general, MIME attachments will have a `Content-Disposition` header with a value of `attachment`. To get the list of body parts matching this criteria, you can use the -[MimeMessage.Attachments](http://www.mimekit.net/docs/html/P_MimeKit_MimeMessage_Attachments.htm) property. +[MimeMessage.Attachments](https://www.mimekit.net/docs/html/P_MimeKit_MimeMessage_Attachments.htm) property. Unfortunately, not all mail clients follow this convention and so you may need to write your own custom logic. For example, you may wish to treat all body parts having a `name` or `filename` parameter set on them: @@ -447,11 +673,17 @@ class HtmlPreviewVisitor : MimeVisitor return false; } - // Save the image to our temp directory and return a "file://" url suitable for - // the browser control to load. - // Note: if you'd rather embed the image data into the HTML, you can construct a - // "data:" url instead. - string SaveImage (MimePart image, string url) + /// + /// Get a file:// URI for the image attachment. + /// + /// + /// Saves the image attachment to a temp file and returns a file:// URI for the + /// temp file. + /// + /// The file:// URI. + /// The image attachment. + /// The original HTML image URL. + string GetFileUri (MimePart image, string url) { string fileName = url.Replace (':', '_').Replace ('\\', '_').Replace ('/', '_'); @@ -459,34 +691,76 @@ class HtmlPreviewVisitor : MimeVisitor if (!File.Exists (path)) { using (var output = File.Create (path)) - image.ContentObject.DecodeTo (output); + image.Content.DecodeTo (output); } return "file://" + path.Replace ('\\', '/'); } + /// + /// Get a data: URI for the image attachment. + /// + /// + /// Encodes the image attachment into a string suitable for setting as a src= attribute value in + /// an img tag. + /// + /// The data: URI. + /// The image attachment. + string GetDataUri (MimePart image) + { + using (var memory = new MemoryStream ()) { + image.Content.DecodeTo (memory); + var buffer = memory.GetBuffer (); + var length = (int) memory.Length; + var base64 = Convert.ToBase64String (buffer, 0, length); + + return string.Format ("data:{0};base64,{1}", image.ContentType.MimeType, base64); + } + } + // Replaces urls that refer to images embedded within the message with // "file://" urls that the browser control will actually be able to load. void HtmlTagCallback (HtmlTagContext ctx, HtmlWriter htmlWriter) { - if (ctx.TagId == HtmlTagId.Image && !ctx.IsEndTag && stack.Count > 0) { + if (ctx.TagId == HtmlTagId.Meta && !ctx.IsEndTag) { + bool isContentType = false; + + ctx.WriteTag (htmlWriter, false); + + // replace charsets with "utf-8" since our output will be in utf-8 (and not whatever the original charset was) + foreach (var attribute in ctx.Attributes) { + if (attribute.Id == HtmlAttributeId.Charset) { + htmlWriter.WriteAttributeName (attribute.Name); + htmlWriter.WriteAttributeValue ("utf-8"); + } else if (isContentType && attribute.Id == HtmlAttributeId.Content) { + htmlWriter.WriteAttributeName (attribute.Name); + htmlWriter.WriteAttributeValue ("text/html; charset=utf-8"); + } else { + if (attribute.Id == HtmlAttributeId.HttpEquiv && attribute.Value != null + && attribute.Value.Equals ("Content-Type", StringComparison.OrdinalIgnoreCase)) + isContentType = true; + + htmlWriter.WriteAttribute (attribute); + } + } + } else if (ctx.TagId == HtmlTagId.Image && !ctx.IsEndTag && stack.Count > 0) { ctx.WriteTag (htmlWriter, false); // replace the src attribute with a file:// URL foreach (var attribute in ctx.Attributes) { if (attribute.Id == HtmlAttributeId.Src) { - MimePart image; - string url; - - if (!TryGetImage (attribute.Value, out image)) { + if (!TryGetImage (attribute.Value, out var image)) { htmlWriter.WriteAttribute (attribute); continue; } - url = SaveImage (image, attribute.Value); + // Note: you can either use a "file://" URI or you can use a + // "data:" URI, the choice is yours. + var uri = GetFileUri (image, attribute.Value); + //var uri = GetDataUri (image); htmlWriter.WriteAttributeName (attribute.Name); - htmlWriter.WriteAttributeValue (url); + htmlWriter.WriteAttributeValue (uri); } else { htmlWriter.WriteAttribute (attribute); } @@ -496,8 +770,8 @@ class HtmlPreviewVisitor : MimeVisitor // add and/or replace oncontextmenu="return false;" foreach (var attribute in ctx.Attributes) { - if (attribute.Name.ToLowerInvariant () == "oncontextmenu") - continue; + if (attribute.Name.Equals ("oncontextmenu", StringComparison.OrdinalIgnoreCase)) + continue; htmlWriter.WriteAttribute (attribute); } @@ -528,7 +802,7 @@ class HtmlPreviewVisitor : MimeVisitor string delsp; if (entity.ContentType.Parameters.TryGetValue ("delsp", out delsp)) - flowed.DeleteSpace = delsp.ToLowerInvariant () == "yes"; + flowed.DeleteSpace = delsp.Equals ("yes", StringComparison.OrdinalIgnoreCase); converter = flowed; } else { @@ -580,25 +854,25 @@ Once you've rendered the message using the above technique, you'll have a list o were not used, even if they did not match the simplistic criteria used by the `MimeMessage.Attachments` property. -### Q: Why doesn't the `MimeMessage` class implement `ISerializable` so that I can serialize a message to disk and read it back later? +### Q: Why doesn't the `MimeMessage` class implement `ISerializable` so that I can serialize a message to disk and read it back later? The MimeKit API was designed to use the existing MIME format for serialization. In light of this, the ability to use the .NET serialization API and format did not make much sense to support. -You can easily serialize a [MimeMessage](http://www.mimekit.net/docs/html/T_MimeKit_MimeMessage.htm) to a stream using the -[WriteTo](http://www.mimekit.net/docs/html/Overload_MimeKit_MimeMessage_WriteTo.htm) methods. +You can easily serialize a [MimeMessage](https://www.mimekit.net/docs/html/T_MimeKit_MimeMessage.htm) to a stream using the +[WriteTo](https://www.mimekit.net/docs/html/Overload_MimeKit_MimeMessage_WriteTo.htm) methods. For more information on this topic, see the following other two topics: -* How can I parse messages? -* How can I save messages? +* [How can I parse messages?](#load-messages) +* [How can I save messages?](#save-messages) -### Q: How can I parse messages? +### Q: How can I parse messages? One of the more common operations that MimeKit is meant for is parsing email messages from arbitrary streams. There are two ways of accomplishing this task. -The first way is to use one of the [Load](http://www.mimekit.net/docs/html/Overload_MimeKit_MimeMessage_Load.htm) methods +The first way is to use one of the [Load](https://www.mimekit.net/docs/html/Overload_MimeKit_MimeMessage_Load.htm) methods on `MimeMessage`: ```csharp @@ -613,7 +887,7 @@ Or you can load a message from a file path: var message = MimeMessage.Load ("message.eml"); ``` -The second way is to use the [MimeParser](http://www.mimekit.net/docs/html/T_MimeKit_MimeParser.htm) class. For the most +The second way is to use the [MimeParser](https://www.mimekit.net/docs/html/T_MimeKit_MimeParser.htm) class. For the most part, using the `MimeParser` directly is not necessary unless you wish to parse a Unix mbox file stream. However, this is how you would do it: @@ -635,10 +909,10 @@ while (!parser.IsEndOfStream) { } ``` -### Q: How can I save messages? +### Q: How can I save messages? -One you've got a [MimeMessage](http://www.mimekit.net/docs/html/T_MimeKit_MimeMessage.htm), you can save -it to a file using the [WriteTo](http://mimekit.net/docs/html/Overload_MimeKit_MimeMessage_WriteTo.htm) method: +One you've got a [MimeMessage](https://www.mimekit.net/docs/html/T_MimeKit_MimeMessage.htm), you can save +it to a file using the [WriteTo](https://mimekit.net/docs/html/Overload_MimeKit_MimeMessage_WriteTo.htm) method: ```csharp message.WriteTo ("message.eml"); @@ -648,7 +922,7 @@ The `WriteTo` method also has overloads that allow you to write the message to a By default, the `WriteTo` method will save the message using DOS line-endings on Windows and Unix line-endings on Unix-based systems such as macOS and Linux. You can override this behavior by -passing a [FormatOptions](http://mimekit.net/docs/html/T_MimeKit_FormatOptions.htm) argument to +passing a [FormatOptions](https://mimekit.net/docs/html/T_MimeKit_FormatOptions.htm) argument to the method: ```csharp @@ -667,19 +941,19 @@ strings due to the fact that each MIME part of the message *may* be encoded in a character set, thus making it impossible to convert the message into a unicode string using a single charset to do the conversion (which is *exactly* what `ToString` does). -### Q: How can I save attachments? +### Q: How can I save attachments? -If you've already got a [MimePart](http://www.mimekit.net/docs/html/T_MimeKit_MimePart.htm) that represents +If you've already got a [MimePart](https://www.mimekit.net/docs/html/T_MimeKit_MimePart.htm) that represents the attachment that you'd like to save, here's how you might save it: ```csharp using (var stream = File.Create (fileName)) - attachment.ContentObject.DecodeTo (stream); + attachment.Content.DecodeTo (stream); ``` Pretty simple, right? -But what if your attachment is actually a [MessagePart](http://www.mimekit.net/docs/html/T_MimeKit_MessagePart.htm)? +But what if your attachment is actually a [MessagePart](https://www.mimekit.net/docs/html/T_MimeKit_MessagePart.htm)? To save the content of a `message/rfc822` part, you'd use the following code snippet: @@ -693,43 +967,49 @@ If you are iterating over all of the attachments in a message, you might do some ```csharp foreach (var attachment in message.Attachments) { var fileName = attachment.ContentDisposition?.FileName ?? attachment.ContentType.Name; - + + if (string.IsNullOrEmpty (fileName)) + fileName = "untitled.dat"; + + // make sure that the filename value does not contain a full path or invalid path characters + fileName = Path.GetFileName (fileName); + using (var stream = File.Create (fileName)) { if (attachment is MessagePart) { var rfc822 = (MessagePart) attachment; - + rfc822.Message.WriteTo (stream); } else { var part = (MimePart) attachment; - - part.ContentObject.DecodeTo (stream); + + part.Content.DecodeTo (stream); } } } ``` -### Q: How can I get the email addresses in the From, To, and Cc headers? +### Q: How can I get the email addresses in the From, To, and Cc headers? -The [From](http://www.mimekit.net/docs/html/P_MimeKit_MimeMessage_From.htm), -[To](http://www.mimekit.net/docs/html/P_MimeKit_MimeMessage_To.htm), and -[Cc](http://www.mimekit.net/docs/html/P_MimeKit_MimeMessage_Cc.htm) properties of a -[MimeMessage](http://www.mimekit.net/docs/html/T_MimeKit_MimeMessage.htm) are all of type -[InternetAddressList](http://www.mimekit.net/docs/html/T_MimeKit_InternetAddressList.htm). An +The [From](https://www.mimekit.net/docs/html/P_MimeKit_MimeMessage_From.htm), +[To](https://www.mimekit.net/docs/html/P_MimeKit_MimeMessage_To.htm), and +[Cc](https://www.mimekit.net/docs/html/P_MimeKit_MimeMessage_Cc.htm) properties of a +[MimeMessage](https://www.mimekit.net/docs/html/T_MimeKit_MimeMessage.htm) are all of type +[InternetAddressList](https://www.mimekit.net/docs/html/T_MimeKit_InternetAddressList.htm). An `InternetAddressList` is a list of -[InternetAddress](http://www.mimekit.net/docs/html/T_MimeKit_InternetAddress.htm) items. This is +[InternetAddress](https://www.mimekit.net/docs/html/T_MimeKit_InternetAddress.htm) items. This is where most people start to get lost because an `InternetAddress` is an abstract class that only -really has a [Name](http://www.mimekit.net/docs/html/P_MimeKit_InternetAddress_Name.htm) property. +really has a [Name](https://www.mimekit.net/docs/html/P_MimeKit_InternetAddress_Name.htm) property. As you've probably already discovered, the `Name` property contains the name of the person (if available), but what you want is his or her email address, not their name. To get the email address, you'll need to figure out what subclass of address each `InternetAddress` really is. There are 2 subclasses of `InternetAddress`: -[GroupAddress](http://www.mimekit.net/docs/html/T_MimeKit_GroupAddress.htm) and -[MailboxAddress](http://www.mimekit.net/docs/html/T_MimeKit_MailboxAddress.htm). +[GroupAddress](https://www.mimekit.net/docs/html/T_MimeKit_GroupAddress.htm) and +[MailboxAddress](https://www.mimekit.net/docs/html/T_MimeKit_MailboxAddress.htm). A `GroupAddress` is a named group of more `InternetAddress` items that are contained within the -[Members](http://www.mimekit.net/docs/html/P_MimeKit_GroupAddress_Members.htm) property. To get +[Members](https://www.mimekit.net/docs/html/P_MimeKit_GroupAddress_Members.htm) property. To get an idea of what a group address represents, consider the following examples: ``` @@ -750,7 +1030,7 @@ To: undisclosed-recipients:; Most of the time, the `From`, `To`, and `Cc` headers will only contain mailbox addresses. As you will notice, a `MailboxAddress` has an -[Address](http://www.mimekit.net/docs/html/P_MimeKit_MailboxAddress_Address.htm) property which will +[Address](https://www.mimekit.net/docs/html/P_MimeKit_MailboxAddress_Address.htm) property which will contain the email address of the mailbox. In the following example, the `Address` property will contain the value `john@smith.com`: @@ -759,14 +1039,14 @@ To: John Smith ``` If you only care about getting a flattened list of the mailbox addresses in a `From`, `To`, or `Cc` -header, you can simply do something like this: +header, you can do something like this: ```csharp foreach (var mailbox in message.To.Mailboxes) Console.WriteLine ("{0}'s email address is {1}", mailbox.Name, mailbox.Address); ``` -### Q: Why do attachments with unicode filenames appear as "ATT0####.dat" in Outlook? +### Q: Why do attachments with Unicode filenames appear as "ATT0####.dat" in Outlook? An attachment filename is stored as a MIME parameter on the `Content-Disposition` header. Unfortunately, the original MIME specifications did not specify a method for encoding non-ASCII filenames. In 1997, @@ -782,10 +1062,8 @@ Outlook is one of those mail clients which decided to encode filenames using the rfc2047 and until Outlook 2007, did not support filenames encoded using the mechanism defined in rfc2231. As of MimeKit v1.2.18, it is possible to configure MimeKit to use the rfc2047 encoding mechanism for -filenames in the following two ways: - -The first way is to set the encoding method on each individual -[Parameter](http://www.mimekit.net/docs/html/T_MimeKit_Parameter.htm): +filenames (and other `Content-Disposition` and `Content-Type` parameter values) by setting the encoding +method on each individual [Parameter](https://www.mimekit.net/docs/html/T_MimeKit_Parameter.htm): ```csharp Parameter param; @@ -794,53 +1072,54 @@ if (attachment.ContentDisposition.Parameters.TryGetValue ("filename", out param) param.EncodingMethod = ParameterEncodingMethod.Rfc2047; ``` -The other way is to use a [FormatOptions](http://www.mimekit.net/docs/html/T_MimeKit_FormatOptions.htm): +Or: ```csharp -var options = FormatOptions.Default.Clone (); -options.ParameterEncodingMethod = ParameterEncodingMethod.Rfc2047; - -message.WriteTo (options, stream); +foreach (var param in attachment.ContentDisposition.Parameters) { + param.EncodingMethod = ParameterEncodingMethod.Rfc2047; +} ``` -### Q: How can I decrypt PGP messages that are embedded in the main message text? +### Q: How can I decrypt PGP messages that are embedded in the main message text? Some PGP-enabled mail clients, such as Thunderbird, embed encrypted PGP blurbs within the `text/plain` body of the message rather than using the PGP/MIME format that MimeKit prefers. These messages often look something like this: - Return-Path: - Received: from [127.0.0.1] (hostname.example.com. [201.95.8.17]) - by mx.google.com with ESMTPSA id l67sm26628445yha.8.2014.04.27.13.49.44 - for - (version=TLSv1 cipher=ECDHE-RSA-RC4-SHA bits=128/128); - Sun, 27 Apr 2014 13:49:44 -0700 (PDT) - Message-ID: <535D6D67.8020803@example.com> - Date: Sun, 27 Apr 2014 17:49:43 -0300 - From: Die-Hard PGP Fan - User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:24.0) Gecko/20100101 Thunderbird/24.4.0 - MIME-Version: 1.0 - To: undisclosed-recipients:; - Subject: Test of inline encrypted PGP blocks - X-Enigmail-Version: 1.6 - Content-Type: text/plain; charset=ISO-8859-1 - Content-Transfer-Encoding: 8bit - X-Antivirus: avast! (VPS 140427-1, 27/04/2014), Outbound message - X-Antivirus-Status: Clean - - -----BEGIN PGP MESSAGE----- - Charset: ISO-8859-1 - Version: GnuPG v2.0.22 (MingW32) - Comment: Using GnuPG with Thunderbird - http://www.enigmail.net/ - - SGFoISBJIGZvb2xlZCB5b3UsIHRoaXMgdGV4dCBpc24ndCBhY3R1YWxseSBlbmNy - eXB0ZWQgd2l0aCBQR1AsCml0J3MgYWN0dWFsbHkgb25seSBiYXNlNjQgZW5jb2Rl - ZCEKCkknbSBqdXN0IHVzaW5nIHRoaXMgYXMgYW4gZXhhbXBsZSwgdGhvdWdoLCBz - byBpdCBkb2Vzbid0IHJlYWxseSBtYXR0ZXIuCgpGb3IgdGhlIHNha2Ugb2YgYXJn - dW1lbnQsIHdlJ2xsIHByZXRlbmQgdGhhdCB0aGlzIGlzIGFjdHVhbGx5IGFuIGVu - Y3J5cHRlZApibHVyYi4gTW1ta2F5PyBUaGFua3MuCg== - -----END PGP MESSAGE----- +```text +Return-Path: +Received: from [127.0.0.1] (hostname.example.com. [201.95.8.17]) + by mx.google.com with ESMTPSA id l67sm26628445yha.8.2014.04.27.13.49.44 + for + (version=TLSv1 cipher=ECDHE-RSA-RC4-SHA bits=128/128); + Sun, 27 Apr 2014 13:49:44 -0700 (PDT) +Message-ID: <535D6D67.8020803@example.com> +Date: Sun, 27 Apr 2014 17:49:43 -0300 +From: Die-Hard PGP Fan +User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:24.0) Gecko/20100101 Thunderbird/24.4.0 +MIME-Version: 1.0 +To: undisclosed-recipients:; +Subject: Test of inline encrypted PGP blocks +X-Enigmail-Version: 1.6 +Content-Type: text/plain; charset=ISO-8859-1 +Content-Transfer-Encoding: 8bit +X-Antivirus: avast! (VPS 140427-1, 27/04/2014), Outbound message +X-Antivirus-Status: Clean + +-----BEGIN PGP MESSAGE----- +Charset: ISO-8859-1 +Version: GnuPG v2.0.22 (MingW32) +Comment: Using GnuPG with Thunderbird - http://www.enigmail.net/ + +SGFoISBJIGZvb2xlZCB5b3UsIHRoaXMgdGV4dCBpc24ndCBhY3R1YWxseSBlbmNy +eXB0ZWQgd2l0aCBQR1AsCml0J3MgYWN0dWFsbHkgb25seSBiYXNlNjQgZW5jb2Rl +ZCEKCkknbSBqdXN0IHVzaW5nIHRoaXMgYXMgYW4gZXhhbXBsZSwgdGhvdWdoLCBz +byBpdCBkb2Vzbid0IHJlYWxseSBtYXR0ZXIuCgpGb3IgdGhlIHNha2Ugb2YgYXJn +dW1lbnQsIHdlJ2xsIHByZXRlbmQgdGhhdCB0aGlzIGlzIGFjdHVhbGx5IGFuIGVu +Y3J5cHRlZApibHVyYi4gTW1ta2F5PyBUaGFua3MuCg== +-----END PGP MESSAGE----- +``` To deal with these kinds of messages, I've added a method to OpenPgpContext called `GetDecryptedStream` which can be used to get the raw decrypted stream. @@ -858,17 +1137,16 @@ public Stream GetDecryptedStream (Stream encryptedData) ``` The first variant is useful in cases where the encrypted PGP blurb is also digitally signed, allowing you to get -your hands on the list of digitial signatures in order for you to verify each of them. +your hands on the list of digital signatures in order for you to verify each of them. -To decrypt the content of the message, you'll want to locate the `TextPart` (in this case, it'll just be -`message.Body`) -and then do this: +To decrypt the content of the message, you'll want to locate the `TextPart` (in this case, it'll just be +`message.Body`) and then do this: -``` +```csharp static Stream DecryptEmbeddedPgp (TextPart text) { using (var memory = new MemoryStream ()) { - text.ContentObject.DecodeTo (memory); + text.Content.DecodeTo (memory); memory.Position = 0; using (var ctx = new MyGnuPGContext ()) { @@ -881,7 +1159,7 @@ static Stream DecryptEmbeddedPgp (TextPart text) What you do with that decrypted stream is up to you. It's up to you to figure out what the decrypted content is (is it text? a jpeg image? a video?) and how to display it to the user. -### Q: How can I reply to a message? +### Q: How can I reply to a message? Replying to a message is fairly simple. For the most part, you'd just create the reply message the same way you'd create any other message. There are only a few slight differences: @@ -895,65 +1173,70 @@ the same way you'd create any other message. There are only a few slight differe 3. You will want to copy the original message's `References` header into the reply message's `References` header and then append the original message's `Message-Id` header. 4. You will probably want to "quote" the original message's text in the reply. +5. If you are generating an automatic reply, you should also follow [RFC3834](https://www.rfc-editor.org/rfc/rfc3834) + and set the `Auto-Submitted` value to `auto-replied`. If this logic were to be expressed in code, it might look something like this: ```csharp public static MimeMessage Reply (MimeMessage message, MailboxAddress from, bool replyToAll) { - var reply = new MimeMessage (); - - reply.From.Add (from); - - // reply to the sender of the message - if (message.ReplyTo.Count > 0) { - reply.To.AddRange (message.ReplyTo); - } else if (message.From.Count > 0) { - reply.To.AddRange (message.From); - } else if (message.Sender != null) { - reply.To.Add (message.Sender); - } - - if (replyToAll) { - // include all of the other original recipients - TODO: remove ourselves from these lists - reply.To.AddRange (message.To); - reply.Cc.AddRange (message.Cc); - } - - // set the reply subject - if (!message.Subject.StartsWith ("Re:", StringComparison.OrdinalIgnoreCase)) - reply.Subject = "Re: " + message.Subject; - else - reply.Subject = message.Subject; - - // construct the In-Reply-To and References headers - if (!string.IsNullOrEmpty (message.MessageId)) { - reply.InReplyTo = message.MessageId; - foreach (var id in message.References) - reply.References.Add (id); - reply.References.Add (message.MessageId); - } - - // quote the original message text - using (var quoted = new StringWriter ()) { - var sender = message.Sender ?? message.From.Mailboxes.FirstOrDefault (); - - quoted.WriteLine ("On {0}, {1} wrote:", message.Date.ToString ("f"), !string.IsNullOrEmpty (sender.Name) ? sender.Name : sender.Address); - using (var reader = new StringReader (message.TextBody)) { - string line; - - while ((line = reader.ReadLine ()) != null) { - quoted.Write ("> "); - quoted.WriteLine (line); - } - } - - reply.Body = new TextPart ("plain") { - Text = quoted.ToString () - }; - } - - return reply; + var reply = new MimeMessage (); + + reply.From.Add (from); + + // reply to the sender of the message + if (message.ReplyTo.Count > 0) { + reply.To.AddRange (message.ReplyTo); + } else if (message.From.Count > 0) { + reply.To.AddRange (message.From); + } else if (message.Sender != null) { + reply.To.Add (message.Sender); + } + + if (replyToAll) { + // include all of the other original recipients - TODO: remove ourselves from these lists + reply.To.AddRange (message.To); + reply.Cc.AddRange (message.Cc); + } + + // set the reply subject + if (!message.Subject?.StartsWith ("Re:", StringComparison.OrdinalIgnoreCase)) + reply.Subject = "Re: " + (message.Subject ?? string.Empty); + else + reply.Subject = message.Subject; + + // construct the In-Reply-To and References headers + if (!string.IsNullOrEmpty (message.MessageId)) { + reply.InReplyTo = message.MessageId; + foreach (var id in message.References) + reply.References.Add (id); + reply.References.Add (message.MessageId); + } + + // if this is an automatic reply, be sure to specify this using the Auto-Submitted header in order to avoid (infinite) mail loops + reply.Headers.Add (HeaderId.AutoSubmitted, "auto-replied"); + + // quote the original message text + using (var quoted = new StringWriter ()) { + var sender = message.Sender ?? message.From.Mailboxes.FirstOrDefault (); + + quoted.WriteLine ("On {0}, {1} wrote:", message.Date.ToString ("f"), !string.IsNullOrEmpty (sender.Name) ? sender.Name : sender.Address); + using (var reader = new StringReader (message.TextBody)) { + string line; + + while ((line = reader.ReadLine ()) != null) { + quoted.Write ("> "); + quoted.WriteLine (line); + } + } + + reply.Body = new TextPart ("plain") { + Text = quoted.ToString () + }; + } + + return reply; } ``` @@ -963,323 +1246,334 @@ body (assuming it has an HTML body) while still including the embedded images? This gets a bit more complicated, but it's still doable... The first thing we'd need to do is implement our own -[MimeVisitor](http://www.mimekit.net/docs/html/T_MimeKit_MimeVisitor.htm) to handle this: +[MimeVisitor](https://www.mimekit.net/docs/html/T_MimeKit_MimeVisitor.htm) to handle this: ```csharp public class ReplyVisitor : MimeVisitor { - readonly Stack stack = new Stack (); - MimeMessage original, reply; - MailboxAddress from; - bool replyToAll; - - /// - /// Creates a new ReplyVisitor. - /// - public ReplyVisitor (MailboxAddress from, bool replyToAll) - { - this.replyToAll = replyToAll; - this.from = from; - } - - /// - /// Gets the reply. - /// - /// The reply. - public MimeMessage Reply { - get { return reply; } - } - - void Push (MimeEntity entity) - { - var multipart = entity as Multipart; - - if (reply.Body == null) { - reply.Body = entity; - } else { - var parent = stack.Peek (); - parent.Add (entity); - } - - if (multipart != null) - stack.Push (multipart); - } - - void Pop () - { - stack.Pop (); - } - - static string GetOnDateSenderWrote (MimeMessage message) - { - var sender = message.Sender != null ? message.Sender : message.From.Mailboxes.FirstOrDefault (); - var name = sender != null ? (!string.IsNullOrEmpty (sender.Name) ? sender.Name : sender.Address) : "an unknown sender"; - - return string.Format ("On {0}, {1} wrote:", message.Date.ToString ("f"), name); - } - - /// - /// Visit the specified message. - /// - /// The message. - public override void Visit (MimeMessage message) - { - reply = new MimeMessage (); - original = message; - - stack.Clear (); - - reply.From.Add (from.Clone ()); - - // reply to the sender of the message - if (message.ReplyTo.Count > 0) { - reply.To.AddRange (message.ReplyTo); - } else if (message.From.Count > 0) { - reply.To.AddRange (message.From); - } else if (message.Sender != null) { - reply.To.Add (message.Sender); - } - - if (replyToAll) { - // include all of the other original recipients - TODO: remove ourselves from these lists - reply.To.AddRange (message.To); - reply.Cc.AddRange (message.Cc); - } - - // set the reply subject - if (!message.Subject.StartsWith ("Re:", StringComparison.OrdinalIgnoreCase)) - reply.Subject = "Re: " + message.Subject; - else - reply.Subject = message.Subject; - - // construct the In-Reply-To and References headers - if (!string.IsNullOrEmpty (message.MessageId)) { - reply.InReplyTo = message.MessageId; - foreach (var id in message.References) - reply.References.Add (id); - reply.References.Add (message.MessageId); - } - - base.Visit (message); - } - - /// - /// Visit the specified entity. - /// - /// The MIME entity. - /// - /// Only Visit(MimeMessage) is supported. - /// - public override void Visit (MimeEntity entity) - { - throw new NotSupportedException (); - } - - protected override void VisitMultipartAlternative (MultipartAlternative alternative) - { - var multipart = new MultipartAlternative (); - - Push (multipart); - - for (int i = 0; i < alternative.Count; i++) - alternative[i].Accept (this); - - Pop (); - } - - protected override void VisitMultipartRelated (MultipartRelated related) - { - var multipart = new MultipartRelated (); - var root = related.Root; - - Push (multipart); - - root.Accept (this); - - for (int i = 0; i < related.Count; i++) { - if (related[i] != root) - related[i].Accept (this); - } - - Pop (); - } - - protected override void VisitMultipart (Multipart multipart) - { - foreach (var part in multipart) { - if (part is MultipartAlternative) - part.Accept (this); - else if (part is MultipartRelated) - part.Accept (this); - else if (part is TextPart) - part.Accept (this); - } - } - - void HtmlTagCallback (HtmlTagContext ctx, HtmlWriter htmlWriter) - { - if (ctx.TagId == HtmlTagId.Body && !ctx.IsEmptyElementTag) { - if (ctx.IsEndTag) { - // end our opening
- htmlWriter.WriteEndTag (HtmlTagId.BlockQuote); - - // pass the tag through to the output - ctx.WriteTag (htmlWriter, true); - } else { - // pass the tag through to the output - ctx.WriteTag (htmlWriter, true); - - // prepend the HTML reply with "On {DATE}, {SENDER} wrote:" - htmlWriter.WriteStartTag (HtmlTagId.P); - htmlWriter.WriteText (GetOnDateSenderWrote (original)); - htmlWriter.WriteEndTag (HtmlTagId.P); - - // Wrap the original content in a
- htmlWriter.WriteStartTag (HtmlTagId.BlockQuote); - htmlWriter.WriteAttribute (HtmlAttributeId.Style, "border-left: 1px #ccc solid; margin: 0 0 0 .8ex; padding-left: 1ex;"); - - ctx.InvokeCallbackForEndTag = true; - } - } else { - // pass the tag through to the output - ctx.WriteTag (htmlWriter, true); - } - } - - string QuoteText (string text) - { - using (var quoted = new StringWriter ()) { - quoted.WriteLine (GetOnDateSenderWrote (original)); - - using (var reader = new StringReader (text)) { - string line; - - while ((line = reader.ReadLine ()) != null) { - quoted.Write ("> "); - quoted.WriteLine (line); - } - } - - return quoted.ToString (); - } - } - - protected override void VisitTextPart (TextPart entity) - { - string text; - - if (entity.IsHtml) { - var converter = new HtmlToHtml { - HtmlTagCallback = HtmlTagCallback - }; - - text = converter.Convert (entity.Text); - } else if (entity.IsFlowed) { - var converter = new FlowedToText (); - - text = converter.Convert (entity.Text); - text = QuoteText (text); - } else { - // quote the original message text - text = QuoteText (entity.Text); - } - - var part = new TextPart (entity.ContentType.MediaSubtype.ToLowerInvariant ()) { - Text = text - }; - - Push (part); - } - - protected override void VisitMessagePart (MessagePart entity) - { - // don't descend into message/rfc822 parts - } + readonly Stack stack = new Stack (); + MimeMessage original, reply; + MailboxAddress from; + bool replyToAll; + int isRelated; + + /// + /// Creates a new ReplyVisitor. + /// + public ReplyVisitor (MailboxAddress from, bool replyToAll) + { + this.replyToAll = replyToAll; + this.from = from; + } + + /// + /// Gets the reply. + /// + /// The reply. + public MimeMessage Reply { + get { return reply; } + } + + void Push (MimeEntity entity) + { + var multipart = entity as Multipart; + + if (reply.Body == null) { + reply.Body = entity; + } else { + var parent = stack.Peek (); + parent.Add (entity); + } + + if (multipart != null) + stack.Push (multipart); + } + + void Pop () + { + stack.Pop (); + } + + static string GetOnDateSenderWrote (MimeMessage message) + { + var sender = message.Sender != null ? message.Sender : message.From.Mailboxes.FirstOrDefault (); + var name = sender != null ? (!string.IsNullOrEmpty (sender.Name) ? sender.Name : sender.Address) : "an unknown sender"; + + return string.Format ("On {0}, {1} wrote:", message.Date.ToString ("f"), name); + } + + /// + /// Visit the specified message. + /// + /// The message. + public override void Visit (MimeMessage message) + { + reply = new MimeMessage (); + original = message; + + stack.Clear (); + + reply.From.Add (from.Clone ()); + + // reply to the sender of the message + if (message.ReplyTo.Count > 0) { + reply.To.AddRange (message.ReplyTo); + } else if (message.From.Count > 0) { + reply.To.AddRange (message.From); + } else if (message.Sender != null) { + reply.To.Add (message.Sender); + } + + if (replyToAll) { + // include all of the other original recipients - TODO: remove ourselves from these lists + reply.To.AddRange (message.To); + reply.Cc.AddRange (message.Cc); + } + + // set the reply subject + if (!message.Subject?.StartsWith ("Re:", StringComparison.OrdinalIgnoreCase)) + reply.Subject = "Re: " + (message.Subject ?? string.Empty); + else + reply.Subject = message.Subject; + + // construct the In-Reply-To and References headers + if (!string.IsNullOrEmpty (message.MessageId)) { + reply.InReplyTo = message.MessageId; + foreach (var id in message.References) + reply.References.Add (id); + reply.References.Add (message.MessageId); + } + + base.Visit (message); + } + + /// + /// Visit the specified entity. + /// + /// The MIME entity. + /// + /// Only Visit(MimeMessage) is supported. + /// + public override void Visit (MimeEntity entity) + { + throw new NotSupportedException (); + } + + protected override void VisitMultipartAlternative (MultipartAlternative alternative) + { + var multipart = new MultipartAlternative (); + + Push (multipart); + + for (int i = 0; i < alternative.Count; i++) + alternative[i].Accept (this); + + Pop (); + } + + protected override void VisitMultipartRelated (MultipartRelated related) + { + var multipart = new MultipartRelated (); + var root = related.Root; + + Push (multipart); + + root.Accept (this); + + isRelated++; + for (int i = 0; i < related.Count; i++) { + if (related[i] != root) + related[i].Accept (this); + } + isRelated--; + + Pop (); + } + + protected override void VisitMultipart (Multipart multipart) + { + foreach (var part in multipart) { + if (part is MultipartAlternative) + part.Accept (this); + else if (part is MultipartRelated) + part.Accept (this); + else if (part is TextPart) + part.Accept (this); + } + } + + void HtmlTagCallback (HtmlTagContext ctx, HtmlWriter htmlWriter) + { + if (ctx.TagId == HtmlTagId.Body && !ctx.IsEmptyElementTag) { + if (ctx.IsEndTag) { + // end our opening
+ htmlWriter.WriteEndTag (HtmlTagId.BlockQuote); + + // pass the tag through to the output + ctx.WriteTag (htmlWriter, true); + } else { + // pass the tag through to the output + ctx.WriteTag (htmlWriter, true); + + // prepend the HTML reply with "On {DATE}, {SENDER} wrote:" + htmlWriter.WriteStartTag (HtmlTagId.P); + htmlWriter.WriteText (GetOnDateSenderWrote (original)); + htmlWriter.WriteEndTag (HtmlTagId.P); + + // Wrap the original content in a
+ htmlWriter.WriteStartTag (HtmlTagId.BlockQuote); + htmlWriter.WriteAttribute (HtmlAttributeId.Style, "border-left: 1px #ccc solid; margin: 0 0 0 .8ex; padding-left: 1ex;"); + + ctx.InvokeCallbackForEndTag = true; + } + } else { + // pass the tag through to the output + ctx.WriteTag (htmlWriter, true); + } + } + + string QuoteText (string text) + { + using (var quoted = new StringWriter ()) { + quoted.WriteLine (GetOnDateSenderWrote (original)); + + using (var reader = new StringReader (text)) { + string line; + + while ((line = reader.ReadLine ()) != null) { + quoted.Write ("> "); + quoted.WriteLine (line); + } + } + + return quoted.ToString (); + } + } + + protected override void VisitTextPart (TextPart entity) + { + string text; + + if (entity.IsHtml) { + var converter = new HtmlToHtml { + HtmlTagCallback = HtmlTagCallback + }; + + text = converter.Convert (entity.Text); + } else if (entity.IsFlowed) { + var converter = new FlowedToText (); + + text = converter.Convert (entity.Text); + text = QuoteText (text); + } else { + // quote the original message text + text = QuoteText (entity.Text); + } + + var part = new TextPart (entity.ContentType.MediaSubtype.ToLowerInvariant ()) { + Text = text + }; + + Push (part); + } + + protected override void VisitMessagePart (MessagePart entity) + { + // don't descend into message/rfc822 parts + } + + protected override void VisitMimePart (MimePart entity) + { + if (isRelated > 0 || !entity.IsAttachment) { + var parent = stack.Peek (); + parent.Add (entity); + } + } } ``` ```csharp public static MimeMessage Reply (MimeMessage message, MailboxAddress from, bool replyToAll) { - var visitor = new ReplyVisitor (from, replyToAll); + var visitor = new ReplyVisitor (from, replyToAll); - visitor.Visit (message); + visitor.Visit (message); - return visitor.Reply; + return visitor.Reply; } ``` -### Q: How can I forward a message? +### Q: How can I forward a message? There are 2 common ways of forwarding a message: attaching the original message as an attachment and inlining the message body much like replying typically does. Which method you choose is up to you. -To forward a message by attaching it as an attachment, you would do do something like this: +To forward a message by attaching it as an attachment, you would do something like this: ```csharp public static MimeMessage Forward (MimeMessage original, MailboxAddress from, IEnumerable to) { - var message = new MimeMessage (); - message.From.Add (from); - message.To.AddRange (to); + var message = new MimeMessage (); + message.From.Add (from); + message.To.AddRange (to); - // set the forwarded subject - if (!original.Subject.StartsWith ("FW:", StringComparison.OrdinalIgnoreCase)) - message.Subject = "FW: " + original.Subject; - else - message.Subject = original.Subject; + // set the forwarded subject + if (!original.Subject?.StartsWith ("FW:", StringComparison.OrdinalIgnoreCase)) + message.Subject = "FW: " + (original.Subject ?? string.Empty); + else + message.Subject = original.Subject; - // create the main textual body of the message - var text = new TextPart ("plain") { Text = "Here's the forwarded message:" }; + // create the main textual body of the message + var text = new TextPart ("plain") { Text = "Here's the forwarded message:" }; - // create the message/rfc822 attachment for the original message - var rfc822 = new MessagePart { Message = original }; + // create the message/rfc822 attachment for the original message + var rfc822 = new MessagePart { Message = original }; - // create a multipart/mixed container for the text body and the forwarded message - var multipart = new Multipart ("mixed"); - multipart.Add (text); - multipart.Add (rfc822); + // create a multipart/mixed container for the text body and the forwarded message + var multipart = new Multipart ("mixed"); + multipart.Add (text); + multipart.Add (rfc822); - // set the multipart as the body of the message - message.Body = multipart; + // set the multipart as the body of the message + message.Body = multipart; - return message; + return message; } ``` -To forward a message by simply inlining the original message's text content, you can do something like this: +To forward a message by inlining the original message's text content, you can do something like this: ```csharp public static MimeMessage Forward (MimeMessage original, MailboxAddress from, IEnumerable to) { - var message = new MimeMessage (); - message.From.Add (from); - message.To.AddRange (to); - - // set the forwarded subject - if (!original.Subject.StartsWith ("FW:", StringComparison.OrdinalIgnoreCase)) - message.Subject = "FW: " + original.Subject; - else - message.Subject = original.Subject; - - // quote the original message text - using (var text = new StringWriter ()) { - text.WriteLine (); - text.WriteLine ("-------- Original Message --------"); - text.WriteLine ("Subject: {0}", original.Subject); - text.WriteLine ("Date: {0}", DateUtils.FormatDate (original.Date)); - text.WriteLine ("From: {0}", original.From); - text.WriteLine ("To: {0}", original.To); - text.WriteLine (); - - text.Write (original.TextBody); - - message.Body = new TextPart ("plain") { - Text = text.ToString () - }; - } - - return message; + var message = new MimeMessage (); + message.From.Add (from); + message.To.AddRange (to); + + // set the forwarded subject + if (!original.Subject?.StartsWith ("FW:", StringComparison.OrdinalIgnoreCase)) + message.Subject = "FW: " + (original.Subject ?? string.Empty); + else + message.Subject = original.Subject; + + // quote the original message text + using (var text = new StringWriter ()) { + text.WriteLine (); + text.WriteLine ("-------- Original Message --------"); + text.WriteLine ("Subject: {0}", original.Subject ?? string.Empty); + text.WriteLine ("Date: {0}", DateUtils.FormatDate (original.Date)); + text.WriteLine ("From: {0}", original.From); + text.WriteLine ("To: {0}", original.To); + text.WriteLine (); + + text.Write (original.TextBody); + + message.Body = new TextPart ("plain") { + Text = text.ToString () + }; + } + + return message; } ``` @@ -1287,15 +1581,15 @@ Keep in mind that not all messages will have a `TextBody` available, so you'll h ## ImapClient -### Q: How can I get the number of unread messages in a folder? +### Q: How can I get the number of unread messages in a folder? -If the folder is open (via [Open](http://www.mimekit.net/docs/html/Overload_MailKit_Net_Imap_ImapFolder_Open.htm)), -then the [ImapFolder.Unread](http://www.mimekit.net/docs/html/P_MailKit_MailFolder_Unread.htm) property will be kept +If the folder is open (via [Open](https://www.mimekit.net/docs/html/Overload_MailKit_Net_Imap_ImapFolder_Open.htm)), +then the [ImapFolder.Unread](https://www.mimekit.net/docs/html/P_MailKit_MailFolder_Unread.htm) property will be kept up to date (at least as-of the latest command issued to the server). If the folder *isn't* open, then you will need to query the unread state of the folder using the -[Status](http://www.mimekit.net/docs/html/M_MailKit_Net_Imap_ImapFolder_Status.htm) method with the -appropriate [StatusItems](http://www.mimekit.net/docs/html/T_MailKit_StatusItems.htm) flag(s). +[Status](https://www.mimekit.net/docs/html/M_MailKit_Net_Imap_ImapFolder_Status.htm) method with the +appropriate [StatusItems](https://www.mimekit.net/docs/html/T_MailKit_StatusItems.htm) flag(s). For example, to get the total *and* unread counts, you can do this: @@ -1306,7 +1600,7 @@ int total = folder.Count; int unread = folder.Unread; ``` -### Q: How can I search for messages delivered between two dates? +### Q: How can I search for messages delivered between two dates? The obvious solution is: @@ -1327,12 +1621,12 @@ var query = SearchQuery.Not (SearchQuery.DeliveredBefore (dateRange.BeginDate) var results = folder.Search (query); ``` -### Q: What does "The ImapClient is currently busy processing a command." mean? +### Q: What does "The ImapClient is currently busy processing a command." mean? If you get an InvalidOperationException with the message, "The ImapClient is currently busy processing a command.", it means that you are trying to use the -[ImapClient](http://www.mimekit.net/docs/html/T_MailKit_Net_Imap_ImapClient.htm) and/or one of its -[ImapFolder](http://www.mimekit.net/docs/html/T_MailKit_Net_Imap_ImapFolder.htm)s from multiple +[ImapClient](https://www.mimekit.net/docs/html/T_MailKit_Net_Imap_ImapClient.htm) and/or one of its +[ImapFolder](https://www.mimekit.net/docs/html/T_MailKit_Net_Imap_ImapFolder.htm)s from multiple threads. To avoid this situation, you'll need to lock the `SyncRoot` property of the `ImapClient` and `ImapFolder` @@ -1349,13 +1643,13 @@ lock (client.SyncRoot) { Note: Locking the `SyncRoot` is only necessary when using the synchronous API's. All `Async()` method variants already do this locking for you. -### Q: Why do I get InvalidOperationException: "The folder is not currently open."? +### Q: Why do I get InvalidOperationException: "The folder is not currently open."? If you get this exception, it's probably because you thought you had to open the destination folder that you passed as an argument to one of the -[CopyTo](http://www.mimekit.net/docs/html/Overload_MailKit_MailFolder_CopyTo.htm) or -[MoveTo](http://www.mimekit.net/docs/html/Overload_MailKit_MailFolder_MoveTo.htm) methods. When you opened -that destination folder, you also inadvertantly closed the source folder which is why you are getting this +[CopyTo](https://www.mimekit.net/docs/html/Overload_MailKit_MailFolder_CopyTo.htm) or +[MoveTo](https://www.mimekit.net/docs/html/Overload_MailKit_MailFolder_MoveTo.htm) methods. When you opened +that destination folder, you also inadvertently closed the source folder which is why you are getting this exception. The IMAP server can only have a single folder open at a time. Whenever you open a folder, you automatically @@ -1363,7 +1657,7 @@ close the previously opened folder. When copying or moving messages from one folder to another, you only need to have the source folder open. -### Q: Why doesn't ImapFolder.MoveTo() move the message out of the source folder? +### Q: Why doesn't ImapFolder.MoveTo() move the message out of the source folder? If you look at the source code for the `ImapFolder.MoveTo()` method, what you'll notice is that there are several code paths depending on the features that the IMAP server supports. @@ -1379,7 +1673,7 @@ messages. If the server supports the `UIDPLUS` extension, then MailKit will attempt to `EXPUNGE` the subset of messages that it just marked for deletion, however, if the `UIDPLUS` extension is not supported by the -IMAP server, then it cannot safely expunge just that subset of messages and so it simply stops there. +IMAP server, then it cannot safely expunge just that subset of messages and so it stops there. My guess is that your server supports neither `MOVE` nor `UIDPLUS` and that is why clients like Outlook continue to see the messages in your folder. I believe, however, that Outlook has a setting to show @@ -1388,14 +1682,14 @@ deleted messages with a strikeout (which you probably have disabled). So to answer your question more succinctly: After calling `folder.MoveTo (...);`, if you are confident that the messages marked for deletion should be expunged, call `folder.Expunge ();` -### Q: How can I mark messages as read for IMAP? +### Q: How can I mark messages as read for IMAP? The way to mark messages as read using the IMAP protocol is to set the `\Seen` flag on the message(s). To do this using MailKit, you will first need to know either the index(es) or the UID(s) of the messages that you would like to set the `\Seen` flag on. Once you have that information, you will want to call one of the -[AddFlags](http://www.mimekit.net/docs/html/Overload_MailKit_MailFolder_AddFlags.htm) methods on the +[AddFlags](https://www.mimekit.net/docs/html/Overload_MailKit_MailFolder_AddFlags.htm) methods on the `ImapFolder`. For example: ```csharp @@ -1408,9 +1702,184 @@ To mark messages as unread, you would *remove* the `\Seen` flag, like so: folder.RemoveFlags (uids, MessageFlags.Seen, true); ``` +### Q: How can I re-synchronize the cache for an IMAP folder? + +Assuming your IMAP server does not support the `QRESYNC` extension (which simplifies this procedure a ton), +here is some simple code to illustrate how to go about re-synchronizing your cache with the remote IMAP +server. + +```csharp +/// +/// Just a simple class to represent the cached information about a message. +/// +class CachedMessageInfo +{ + public UniqueId UniqueId; + public MessageFlags Flags; + public HashSet Keywords; + public Envelope Envelope; + public BodyPart Body; +} + +/// +/// Resynchronize the cache with the remote IMAP folder. +/// +/// The IMAP folder. +/// The local cache of message metadata. +/// The cached UIDVALIDITY value of the IMAP folder from a previous session. +static void ResyncFolder (ImapFolder folder, List cache, ref uint cachedUidValidity) +{ + IList summaries; + + // Step 1: Open the folder. + + // Note: we only need read-only access to update our cache, but depending on + // what you plan to do with the folder after resynchronizing, you may want + // top open the folder in read-write mode instead. + folder.Open (FolderAccess.ReadOnly); + + if (cache.Count > 0) { + if (folder.UidValidity == cachedUidValidity) { + // Step 2: Remove messages from our cache that no longer exist on the server. + + // get the full list of UIDs on the server... + var all = folder.Search (SearchQuery.All); + + // remove any messages from our cache that no longer exist... + for (int i = 0; i < cache.Count; i++) { + if (!all.Contains (cache[i].UniqueId)) { + cache.RemoveAt (i); + i--; + } + } + + // Step 3: Sync any flag changes for our cached messages. + + // get a list of known uids... astute observers will note that an easy + // optimization to make here would be to merge this loop with the above + // loop. + var known = new UniqueIdSet (SortOrder.Ascending); + for (int i = 0; i < cache.Count; i++) + known.Add (cache[i].UniqueId); + + // fetch the flags for our known messages... + summaries = folder.Fetch (known, MessageSummaryItems.Flags); + for (int i = 0; i < summaries.Count; i++) { + // Note: the indexes should match up with our cache, but it wouldn't + // hurt to add error checking to make sure. I'm not bothering to here + // for simplicity reasons. + cache[i].Flags = summaries[i].Flags.Value; + cache[i].Keywords = summaries[i].Keywords; + } + } else { + // The UIDVALIDITY of the folder has changed. This means that our entire + // cache is obsolete. We need to clear our cache and start from scratch. + cachedUidValidity = folder.UidValidity; + cache.Clear (); + } + } else { + // We have nothing cached, so just start from scratch. + cachedUidValidity = folder.UidValidity; + } + + // Step 4: Fetch the messages we don't already know about and add them to our cache. + + summaries = folder.Fetch (cache.Count, -1, MessageSummaryItems.UniqueId | MessageSummaryItems.Flags | MessageSummaryItems.Envelope | MessageSummaryItems.BodyStructure); + for (int i = 0; i < summaries.Count; i++) { + cache.Add (new CachedMessageInfo { + UniqueId = summaries[i].UniqueId, + Flags = summaries[i].Flags.Value, + Keywords = summaries[i].Keywords, + Envelope = summaries[i].Envelope, + Body = summaries[i].Body + }); + } + + // Tada! Now we are resynchronized with the server! +} +``` + +### Q: How can I login using a shared mailbox in Office365? + +```csharp +var result = await GetPublicClientOAuth2CredentialsAsync ("IMAP", "sharedMailboxName@custom-domain.com"); + +// Note: We always use result.Account.Username instead of `Username` because the user may have selected an alternative account. +var oauth2 = new SaslMechanismOAuth2 (result.Account.Username, result.AccessToken); + +using (var client = new ImapClient ()) { + await client.ConnectAsync ("outlook.office365.com", 993, SecureSocketOptions.SslOnConnect); + await client.AuthenticateAsync (oauth2); + + // ... + + await client.DisconnectAsync (true); +} +``` + +Notes: + +1. The `GetPublicClientOAuth2CredentialsAsync()` method used in this example code snippet can be found in the +[ExchangeOAuth2.md](ExchangeOAuth2.md#desktop-and-mobile-applications) documentation. +2. Some users have reported that they need to use `"username@custom-domain.com\\sharedMailboxName"` as their +username instead of `"sharedMailboxName@custom-domain.com"`. + ## SmtpClient -### Q: How can I send email to a SpecifiedPickupDirectory? +### Q: Why doesn't the message show up in the "Sent Mail" folder after sending it? + +It seems to be a common misunderstanding that messages sent via SMTP will magically show up in the account's "Sent Mail" folder. + +In order for the message to show up in the "Sent Mail" folder, you will need to append the message to the "Sent Mail" folder +yourself because the SMTP protocol does not support doing this automatically. + +If the "Sent Mail" folder is a local mbox folder, you'll need to append it like this: + +```csharp +using (var mbox = File.Open ("C:\\path\\to\\Sent Mail.mbox", FileMode.Append, FileAccess.Write)) { + var marker = string.Format ("From MAILER-DAEMON {0}{1}", DateTime.Now.ToString (CultureInfo.InvariantCulture, "ddd MMM d HH:mm:ss yyyy"), Environment.NewLine); + var bytes = Encoding.ASCII.GetBytes (marker); + + // Write the mbox marker bytes. + mbox.Write (bytes, 0, bytes.Length); + + // Write the message, making sure to escape any line that looks like an mbox From-marker. + using (var filtered = new FilteredStream (stream)) { + filtered.Add (new MboxFromMarker ()); + message.WriteTo (filtered); + filtered.Flush (); + } + + mbox.Flush (); +} +``` + +If the "Sent Mail" folder exists on an IMAP server, you would need to do something more like this: + +```csharp +using (var client = new ImapClient ()) { + client.Connect ("imap.server.com", 993, SecureSocketOptions.SslOnConnect); + client.Authenticate ("username", "password"); + + IMailFolder sentMail; + + if (client.Capabilities.HasFlag (ImapCapabilities.SpecialUse)) { + sentMail = client.GetFolder (SpecialFolder.Sent); + } else { + var personal = client.GetFolder (client.PersonalNamespaces[0]); + + // Note: This assumes that the "Sent Mail" folder lives at the root of the folder hierarchy + // and is named "Sent Mail" as opposed to "Sent" or "Sent Items" or any other variation. + sentMail = personal.GetSubfolder ("Sent Mail"); + } + + sentMail.Append (message, MessageFlags.Seen); + + client.Disconnect (true); +} +``` + +### Q: How can I send email to a SpecifiedPickupDirectory? Based on Microsoft's [referencesource](https://github.com/Microsoft/referencesource/blob/master/System/net/System/Net/mail/SmtpClient.cs#L401), when `SmtpDeliveryMethod.SpecifiedPickupDirectory` is used, the `SmtpClient` saves the message to the @@ -1419,27 +1888,60 @@ specified pickup directory location using a randomly generated filename based on like this: ```csharp -void SendToPickupDirectory (MimeMessage message, string pickupDirectory) +public static void SaveToPickupDirectory (MimeMessage message, string pickupDirectory) { do { + // Generate a random file name to save the message to. var path = Path.Combine (pickupDirectory, Guid.NewGuid ().ToString () + ".eml"); + Stream stream; - if (File.Exists (path)) - continue; + try { + // Attempt to create the new file. + stream = File.Open (path, FileMode.CreateNew); + } catch (IOException) { + // If the file already exists, try again with a new Guid. + if (File.Exists (path)) + continue; + + // Otherwise, fail immediately since it probably means that there is + // no graceful way to recover from this error. + throw; + } try { - using (var stream = new FileStream (path, FileMode.CreateNew)) { - message.WriteTo (stream); - return; + using (stream) { + // IIS pickup directories expect the message to be "byte-stuffed" + // which means that lines beginning with "." need to be escaped + // by adding an extra "." to the beginning of the line. + // + // Use an SmtpDataFilter "byte-stuff" the message as it is written + // to the file stream. This is the same process that an SmtpClient + // would use when sending the message in a `DATA` command. + using (var filtered = new FilteredStream (stream)) { + filtered.Add (new SmtpDataFilter ()); + + // Make sure to write the message in DOS () format. + var options = FormatOptions.Default.Clone (); + options.NewLineFormat = NewLineFormat.Dos; + + message.WriteTo (options, filtered); + filtered.Flush (); + return; + } } - } catch (IOException) { - // The file may have been created between our File.Exists() check and - // our attempt to create the stream. + } catch { + // An exception here probably means that the disk is full. + // + // Delete the file that was created above so that incomplete files are not + // left behind for IIS to send accidentally. + File.Delete (path); + throw; } } while (true); } ``` -### Q: How can I request a notification when the message is read by the user? + +### Q: How can I request a notification when the message is read by the user? The first thing I need to make clear is that requesting a notification does not guarantee that you'll actually get one. In order for you to receive a notification that the message was read by its recipient, the recipient's @@ -1455,7 +1957,7 @@ message.Headers[HeaderId.DispositionNotificationTo] = new MailboxAddress ("My Na For more information on this topic, read [rfc3798](https://tools.ietf.org/html/rfc3798). -### Q: How can I process a read receipt notification? +### Q: How can I process a read receipt notification? A read receipt notification comes in the form of a MIME message with a top-level MIME part with a MIME-type of `multipart/report` that has a `report-type` parameter with a value of `disposition-notification`. @@ -1472,7 +1974,7 @@ if (report != null && report.ReportType.Equals ("disposition-notification", Stri The first part of the `multipart/report` will be a human-readable explanation of the notification. The second part will have a MIME-type of `message/disposition-notification` and be represented by -a [MessageDispositionNotification](http://www.mimekit.net/docs/html/T_MimeKit_MessageDispositionNotification.htm). +a [MessageDispositionNotification](https://www.mimekit.net/docs/html/T_MimeKit_MessageDispositionNotification.htm). This notification part will contain a list of header-like fields containing information about the message that this notification is for such as the `Original-Message-Id`, `Original-Recipient`, etc. @@ -1484,4 +1986,5 @@ if (notification != null) { var messageId = notification.Fields["Original-Message-Id"]; } ``` + For more information on this topic, read [rfc3798](https://tools.ietf.org/html/rfc3798). diff --git a/GMailOAuth2.md b/GMailOAuth2.md new file mode 100644 index 0000000000..fb8f857ad2 --- /dev/null +++ b/GMailOAuth2.md @@ -0,0 +1,169 @@ +# Using OAuth2 With GMail (IMAP, POP3 or SMTP) + +## Quick Index + +* [Setting up OAuth2 for use with Google Mail](#setting-up-oauth2-for-use-with-google-mail) + * [Register Your Application with Google](#register-your-application-with-google) + * [Obtaining an OAuth2 Client ID and Secret](#obtaining-an-oauth2-client-id-and-secret) +* [Authenticating a Desktop App with the OAuth2 Client ID and Secret](#authenticating-a-desktop-app-with-the-oauth2-client-id-and-secret) +* [Authenticating an ASP.NET Web App with the OAuth2 Client ID and Secret](#authenticating-an-aspnet-web-app-with-the-oauth2-client-id-and-secret) + +## Setting up OAuth2 for use with Google Mail + +### Register Your Application with Google + +Go to [Google's Developer Console](https://cloud.google.com/console). + +Click the **Select A Project** button in the **Navigation Bar** at the top of the screen. + +![Click "Select A Project"](https://github.com/jstedfast/MailKit/blob/master/Documentation/media/google-developer-console/click-select-a-project.png) + +Click the **New Project** button. + +![Click "New Project"](https://github.com/jstedfast/MailKit/blob/master/Documentation/media/google-developer-console/click-new-project.png) + +Fill in the name **Project Name**, and if appropriate, select the **Organization** that your program +should be associated with. Then click *Create*. + +![Create New Project](https://github.com/jstedfast/MailKit/blob/master/Documentation/media/google-developer-console/create-new-project.png) + +### Obtaining an OAuth2 Client ID and Secret + +Click the **☰** symbol, move down to **APIs & Services** and then select **OAuth consent screen**. + +![Click "OAuth consent screen"](https://github.com/jstedfast/MailKit/blob/master/Documentation/media/google-developer-console/click-oauth-consent-screen-menu.png) + +Select the **External** radio item and then click **Create**. + +![Select "External"](https://github.com/jstedfast/MailKit/blob/master/Documentation/media/google-developer-console/select-external.png) + +Fill in the **Application name** and any other fields that are appropriate for your application and then click +**Create**. + +![OAuth consent screen](https://github.com/jstedfast/MailKit/blob/master/Documentation/media/google-developer-console/oauth-consent-screen.png) + +Click **+ Create Credentials** and then select **OAuth client ID**. + +![Click "Create Credentials"](https://github.com/jstedfast/MailKit/blob/master/Documentation/media/google-developer-console/click-create-credentials.png) + +Select the **Other** radio item in the **Application type** section and then type in a name to use for the OAuth +client ID. Once completed, click **Create**. + +![Select "Other"](https://github.com/jstedfast/MailKit/blob/master/Documentation/media/google-developer-console/select-application-type-other.png) + +At this point, you will be presented with a web dialog that will allow you to copy the **Client ID** and +**Client Secret** strings into your clipboard to paste them into your program. + +![Client ID and Secret](https://github.com/jstedfast/MailKit/blob/master/Documentation/media/google-developer-console/client-id-and-secret.png) + +## Authenticating a Desktop App with the OAuth2 Client ID and Secret + +Now that you have the **Client ID** and **Client Secret** strings, you'll need to plug those values into +your application. + +The following sample code uses the [Google.Apis.Auth](https://www.nuget.org/packages/Google.Apis.Auth/) +nuget package for obtaining the access token which will be needed by MailKit to pass on to the GMail +server. + +```csharp +const string GMailAccount = "username@gmail.com"; + +var clientSecrets = new ClientSecrets { + ClientId = "XXX.apps.googleusercontent.com", + ClientSecret = "XXX" +}; + +var codeFlow = new GoogleAuthorizationCodeFlow (new GoogleAuthorizationCodeFlow.Initializer { + // Cache tokens in ~/.local/share/google-filedatastore/CredentialCacheFolder on Linux/Mac + DataStore = new FileDataStore ("CredentialCacheFolder", false), + Scopes = new [] { "https://mail.google.com/" }, + ClientSecrets = clientSecrets, + LoginHint = GMailAccount +}); + +// Note: For a web app, you'll want to use AuthorizationCodeWebApp instead. +var codeReceiver = new LocalServerCodeReceiver (); +var authCode = new AuthorizationCodeInstalledApp (codeFlow, codeReceiver); + +var credential = await authCode.AuthorizeAsync (GMailAccount, CancellationToken.None); + +if (credential.Token.IsStale) + await credential.RefreshTokenAsync (CancellationToken.None); + +var oauth2 = new SaslMechanismOAuthBearer (credential.UserId, credential.Token.AccessToken); + +using (var client = new ImapClient ()) { + await client.ConnectAsync ("imap.gmail.com", 993, SecureSocketOptions.SslOnConnect); + await client.AuthenticateAsync (oauth2); + await client.DisconnectAsync (true); +} +``` + +## Authenticating an ASP.NET Web App with the OAuth2 Client ID and Secret + +Now that you have the **Client ID** and **Client Secret** strings, you'll need to plug those values into +your application. + +The following sample code uses the [Google.Apis.Auth](https://www.nuget.org/packages/Google.Apis.Auth/) +nuget package for obtaining the access token which will be needed by MailKit to pass on to the GMail +server. + +Add Google Authentication processor to your **Program.cs**. + +```csharp +builder.Services.AddAuthentication (options => { + // This forces challenge results to be handled by Google OpenID Handler, so there's no + // need to add an AccountController that emits challenges for Login. + options.DefaultChallengeScheme = GoogleOpenIdConnectDefaults.AuthenticationScheme; + + // This forces forbid results to be handled by Google OpenID Handler, which checks if + // extra scopes are required and does automatic incremental auth. + options.DefaultForbidScheme = GoogleOpenIdConnectDefaults.AuthenticationScheme; + + // Default scheme that will handle everything else. + // Once a user is authenticated, the OAuth2 token info is stored in cookies. + options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; +}) +.AddCookie (options => { + options.ExpireTimeSpan = TimeSpan.FromMinutes (5); +}) +.AddGoogleOpenIdConnect (options => { + var secrets = GoogleClientSecrets.FromFile ("client_secret.json").Secrets; + options.ClientId = secrets.ClientId; + options.ClientSecret = secrets.ClientSecret; +}); +``` + +Ensure that you are using Authorization and HttpsRedirection in your **Program.cs**: + +```csharp +app.UseHttpsRedirection (); +app.UseStaticFiles (); + +app.UseRouting (); + +app.UseAuthentication (); +app.UseAuthorization (); +``` + +Now, using the **GoogleScopedAuthorizeAttribute**, you can request scopes saved in a library as constants and request tokens for these scopes. + +```csharp +[GoogleScopedAuthorize(DriveService.ScopeConstants.DriveReadonly)] +public async Task AuthenticateAsync ([FromServices] IGoogleAuthProvider auth) +{ + GoogleCredential? googleCred = await auth.GetCredentialAsync (); + string token = await googleCred.UnderlyingCredential.GetAccessTokenForRequestAsync (); + + var oauth2 = new SaslMechanismOAuthBearer ("UserEmail", token); + + using var emailClient = new ImapClient (); + await emailClient.ConnectAsync ("imap.gmail.com", 993, SecureSocketOptions.SslOnConnect); + await emailClient.AuthenticateAsync (oauth2); + await emailClient.DisconnectAsync (true); +} +``` + +All of that and more has been described in Google's [OAuth 2.0](https://developers.google.com/api-client-library/dotnet/guide/aaa_oauth#web-applications-aspnet-mvc) +documentation. However, be careful since [Asp.Net MVC](https://developers.google.com/api-client-library/dotnet/guide/aaa_oauth#web-applications-asp.net-mvc) +does not work for Asp.Net Core. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000..b316677fc6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (C) 2013-2026 .NET Foundation and Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/License.md b/License.md deleted file mode 100644 index 2f6dc6c859..0000000000 --- a/License.md +++ /dev/null @@ -1,21 +0,0 @@ -## License Information - -MailKit is Copyright (C) 2013-2016 Xamarin Inc. and is licensed under the MIT license: - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. diff --git a/MailKit.Coverity.sln b/MailKit.Coverity.sln index 66444b7a40..e22ac56651 100644 --- a/MailKit.Coverity.sln +++ b/MailKit.Coverity.sln @@ -1,36 +1,37 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 2012 -VisualStudioVersion = 12.0.31101.0 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30711.63 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MailKit.Net45", "MailKit\MailKit.Net45.csproj", "{7264D469-A390-4C10-9C87-DAA37EDD3C1D}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MimeKit.Net45", "submodules\MimeKit\MimeKit\MimeKit.Net45.csproj", "{D5F54A4F-D84B-430F-9271-F7861E285B3E}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{BB3237C7-E19C-4232-B875-6658ABDD184A}" ProjectSection(SolutionItems) = preProject .nuget\packages.config = .nuget\packages.config EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MimeKit", "submodules\MimeKit\MimeKit\MimeKit.csproj", "{4453C1EF-9C6A-4305-B70B-9154AE48B63C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MailKit", "MailKit\MailKit.csproj", "{67EBBC81-9334-49CE-BF7B-17DA659E9736}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {7264D469-A390-4C10-9C87-DAA37EDD3C1D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7264D469-A390-4C10-9C87-DAA37EDD3C1D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7264D469-A390-4C10-9C87-DAA37EDD3C1D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7264D469-A390-4C10-9C87-DAA37EDD3C1D}.Release|Any CPU.Build.0 = Release|Any CPU - {D5F54A4F-D84B-430F-9271-F7861E285B3E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D5F54A4F-D84B-430F-9271-F7861E285B3E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D5F54A4F-D84B-430F-9271-F7861E285B3E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D5F54A4F-D84B-430F-9271-F7861E285B3E}.Release|Any CPU.Build.0 = Release|Any CPU + {4453C1EF-9C6A-4305-B70B-9154AE48B63C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4453C1EF-9C6A-4305-B70B-9154AE48B63C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4453C1EF-9C6A-4305-B70B-9154AE48B63C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4453C1EF-9C6A-4305-B70B-9154AE48B63C}.Release|Any CPU.Build.0 = Release|Any CPU + {67EBBC81-9334-49CE-BF7B-17DA659E9736}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {67EBBC81-9334-49CE-BF7B-17DA659E9736}.Debug|Any CPU.Build.0 = Debug|Any CPU + {67EBBC81-9334-49CE-BF7B-17DA659E9736}.Release|Any CPU.ActiveCfg = Release|Any CPU + {67EBBC81-9334-49CE-BF7B-17DA659E9736}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection - GlobalSection(NestedProjects) = preSolution + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {AE4FD452-949D-40F5-B83B-B439EBA814AA} EndGlobalSection GlobalSection(MonoDevelopProperties) = preSolution StartupItem = MailKit\MailKit.Net45.csproj diff --git a/MailKit.Documentation.sln b/MailKit.Documentation.sln index 35ba8c6eca..82ae58e147 100644 --- a/MailKit.Documentation.sln +++ b/MailKit.Documentation.sln @@ -1,20 +1,13 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 2013 -VisualStudioVersion = 12.0.31101.0 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29926.136 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MailKit.Net45", "MailKit\MailKit.Net45.csproj", "{7264D469-A390-4C10-9C87-DAA37EDD3C1D}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MimeKit.Net45", "submodules\MimeKit\MimeKit\MimeKit.Net45.csproj", "{D5F54A4F-D84B-430F-9271-F7861E285B3E}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BouncyCastle", "submodules\MimeKit\submodules\bc-csharp\crypto\BouncyCastle.csproj", "{4C235092-820C-4DEB-9074-D356FB797D8B}" -EndProject Project("{7CF6DF6D-3B04-46F8-A40B-537D21BCA0B4}") = "Documentation", "Documentation\Documentation.shfbproj", "{59115814-A1E3-46AE-AE30-4065AE8F4CAF}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{2299B3E8-95E4-4766-9AA2-5553EAD7F375}" - ProjectSection(SolutionItems) = preProject - .nuget\packages.config = .nuget\packages.config - EndProjectSection +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MimeKit", "submodules\MimeKit\MimeKit\MimeKit.csproj", "{FAEC8A91-6983-4ED9-A414-09C6B65B13BB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MailKit", "MailKit\MailKit.csproj", "{E543A427-93DE-4E65-ADF2-44412E440FB1}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -22,25 +15,24 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {7264D469-A390-4C10-9C87-DAA37EDD3C1D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7264D469-A390-4C10-9C87-DAA37EDD3C1D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7264D469-A390-4C10-9C87-DAA37EDD3C1D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7264D469-A390-4C10-9C87-DAA37EDD3C1D}.Release|Any CPU.Build.0 = Release|Any CPU - {D5F54A4F-D84B-430F-9271-F7861E285B3E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D5F54A4F-D84B-430F-9271-F7861E285B3E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D5F54A4F-D84B-430F-9271-F7861E285B3E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D5F54A4F-D84B-430F-9271-F7861E285B3E}.Release|Any CPU.Build.0 = Release|Any CPU - {4C235092-820C-4DEB-9074-D356FB797D8B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4C235092-820C-4DEB-9074-D356FB797D8B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4C235092-820C-4DEB-9074-D356FB797D8B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4C235092-820C-4DEB-9074-D356FB797D8B}.Release|Any CPU.Build.0 = Release|Any CPU {59115814-A1E3-46AE-AE30-4065AE8F4CAF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {59115814-A1E3-46AE-AE30-4065AE8F4CAF}.Release|Any CPU.ActiveCfg = Release|Any CPU {59115814-A1E3-46AE-AE30-4065AE8F4CAF}.Release|Any CPU.Build.0 = Release|Any CPU + {FAEC8A91-6983-4ED9-A414-09C6B65B13BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FAEC8A91-6983-4ED9-A414-09C6B65B13BB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FAEC8A91-6983-4ED9-A414-09C6B65B13BB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FAEC8A91-6983-4ED9-A414-09C6B65B13BB}.Release|Any CPU.Build.0 = Release|Any CPU + {E543A427-93DE-4E65-ADF2-44412E440FB1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E543A427-93DE-4E65-ADF2-44412E440FB1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E543A427-93DE-4E65-ADF2-44412E440FB1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E543A427-93DE-4E65-ADF2-44412E440FB1}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {EB22C82D-37B9-4F33-9228-B5FD1B590B1F} + EndGlobalSection GlobalSection(MonoDevelopProperties) = preSolution Policies = $0 $0.StandardHeader = $1 diff --git a/MailKit.Mobile.sln b/MailKit.Mobile.sln deleted file mode 100644 index 9a1e97cca9..0000000000 --- a/MailKit.Mobile.sln +++ /dev/null @@ -1,85 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 11.00 -# Visual Studio 2010 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BouncyCastle.Android", "submodules\MimeKit\submodules\bc-csharp\crypto\BouncyCastle.Android.csproj", "{A0D302CB-8866-4AB1-98B9-F0772EABF5DF}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BouncyCastle.iOS", "submodules\MimeKit\submodules\bc-csharp\crypto\BouncyCastle.iOS.csproj", "{0249241C-205E-4AC0-828B-90F822359B9E}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MimeKit.Android", "submodules\MimeKit\MimeKit\MimeKit.Android.csproj", "{004B4019-62B7-4A15-AF2C-C20968845C46}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MimeKit.iOS", "submodules\MimeKit\MimeKit\MimeKit.iOS.csproj", "{4C1288AD-12C8-4BF7-AED7-6C4DC539C856}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MailKit.Android", "MailKit\MailKit.Android.csproj", "{9BF818C3-C20E-4EFB-9426-F0A6C2EDC627}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MailKit.iOS", "MailKit\MailKit.iOS.csproj", "{60B5D72B-8219-48B6-B688-AD0FE284A96A}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - AppStore|Any CPU = AppStore|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {004B4019-62B7-4A15-AF2C-C20968845C46}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU - {004B4019-62B7-4A15-AF2C-C20968845C46}.AppStore|Any CPU.Build.0 = Debug|Any CPU - {004B4019-62B7-4A15-AF2C-C20968845C46}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {004B4019-62B7-4A15-AF2C-C20968845C46}.Debug|Any CPU.Build.0 = Debug|Any CPU - {004B4019-62B7-4A15-AF2C-C20968845C46}.Release|Any CPU.ActiveCfg = Release|Any CPU - {004B4019-62B7-4A15-AF2C-C20968845C46}.Release|Any CPU.Build.0 = Release|Any CPU - {0249241C-205E-4AC0-828B-90F822359B9E}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU - {0249241C-205E-4AC0-828B-90F822359B9E}.AppStore|Any CPU.Build.0 = Debug|Any CPU - {0249241C-205E-4AC0-828B-90F822359B9E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0249241C-205E-4AC0-828B-90F822359B9E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0249241C-205E-4AC0-828B-90F822359B9E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0249241C-205E-4AC0-828B-90F822359B9E}.Release|Any CPU.Build.0 = Release|Any CPU - {4C1288AD-12C8-4BF7-AED7-6C4DC539C856}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU - {4C1288AD-12C8-4BF7-AED7-6C4DC539C856}.AppStore|Any CPU.Build.0 = Debug|Any CPU - {4C1288AD-12C8-4BF7-AED7-6C4DC539C856}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4C1288AD-12C8-4BF7-AED7-6C4DC539C856}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4C1288AD-12C8-4BF7-AED7-6C4DC539C856}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4C1288AD-12C8-4BF7-AED7-6C4DC539C856}.Release|Any CPU.Build.0 = Release|Any CPU - {60B5D72B-8219-48B6-B688-AD0FE284A96A}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU - {60B5D72B-8219-48B6-B688-AD0FE284A96A}.AppStore|Any CPU.Build.0 = Debug|Any CPU - {60B5D72B-8219-48B6-B688-AD0FE284A96A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {60B5D72B-8219-48B6-B688-AD0FE284A96A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {60B5D72B-8219-48B6-B688-AD0FE284A96A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {60B5D72B-8219-48B6-B688-AD0FE284A96A}.Release|Any CPU.Build.0 = Release|Any CPU - {9BF818C3-C20E-4EFB-9426-F0A6C2EDC627}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU - {9BF818C3-C20E-4EFB-9426-F0A6C2EDC627}.AppStore|Any CPU.Build.0 = Debug|Any CPU - {9BF818C3-C20E-4EFB-9426-F0A6C2EDC627}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9BF818C3-C20E-4EFB-9426-F0A6C2EDC627}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9BF818C3-C20E-4EFB-9426-F0A6C2EDC627}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9BF818C3-C20E-4EFB-9426-F0A6C2EDC627}.Release|Any CPU.Build.0 = Release|Any CPU - {A0D302CB-8866-4AB1-98B9-F0772EABF5DF}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU - {A0D302CB-8866-4AB1-98B9-F0772EABF5DF}.AppStore|Any CPU.Build.0 = Debug|Any CPU - {A0D302CB-8866-4AB1-98B9-F0772EABF5DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A0D302CB-8866-4AB1-98B9-F0772EABF5DF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A0D302CB-8866-4AB1-98B9-F0772EABF5DF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A0D302CB-8866-4AB1-98B9-F0772EABF5DF}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(MonoDevelopProperties) = preSolution - StartupItem = MailKit\MailKit.iOS..csproj - Policies = $0 - $0.TextStylePolicy = $1 - $1.inheritsSet = null - $1.scope = text/x-csharp - $0.CSharpFormattingPolicy = $2 - $2.AlignEmbeddedUsingStatements = False - $2.AlignEmbeddedIfStatements = False - $2.NamespaceBraceStyle = EndOfLine - $2.StructBraceStyle = EndOfLine - $2.EnumBraceStyle = EndOfLine - $2.BeforeIndexerDeclarationBracket = False - $2.AfterDelegateDeclarationParameterComma = True - $2.BeforeSizeOfParentheses = True - $2.BeforeTypeOfParentheses = True - $2.SpacesAfterTypecast = True - $2.BlankLinesBeforeUsings = 1 - $2.inheritsSet = Mono - $2.inheritsScope = text/x-csharp - $2.scope = text/x-csharp - $0.StandardHeader = $3 - $3.Text = @\r\n${FileName}\n \nAuthor: ${AuthorName} <${AuthorEmail}>\n\nCopyright (c) ${Year} ${CopyrightHolder}\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the "Software"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n - $3.IncludeInNewFiles = True - EndGlobalSection -EndGlobal diff --git a/MailKit.Net40.sln b/MailKit.Net40.sln deleted file mode 100644 index 24382ab3d6..0000000000 --- a/MailKit.Net40.sln +++ /dev/null @@ -1,52 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 11.00 -# Visual Studio 2010 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MailKit.Net40", "MailKit\MailKit.Net40.csproj", "{DB3A2478-4742-452B-80C1-F672B64285AD}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MimeKit.Net40", "submodules\MimeKit\MimeKit\MimeKit.Net40.csproj", "{C909FC86-6084-41E5-B99C-DCDF2A5B7F82}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {C909FC86-6084-41E5-B99C-DCDF2A5B7F82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C909FC86-6084-41E5-B99C-DCDF2A5B7F82}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C909FC86-6084-41E5-B99C-DCDF2A5B7F82}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C909FC86-6084-41E5-B99C-DCDF2A5B7F82}.Release|Any CPU.Build.0 = Release|Any CPU - {DB3A2478-4742-452B-80C1-F672B64285AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DB3A2478-4742-452B-80C1-F672B64285AD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DB3A2478-4742-452B-80C1-F672B64285AD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DB3A2478-4742-452B-80C1-F672B64285AD}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(MonoDevelopProperties) = preSolution - StartupItem = MailKit\MailKit.Net40.csproj - Policies = $0 - $0.TextStylePolicy = $1 - $1.inheritsSet = null - $1.scope = text/x-csharp - $0.CSharpFormattingPolicy = $2 - $2.AlignEmbeddedUsingStatements = False - $2.AlignEmbeddedIfStatements = False - $2.NamespaceBraceStyle = EndOfLine - $2.StructBraceStyle = EndOfLine - $2.EnumBraceStyle = EndOfLine - $2.BeforeIndexerDeclarationBracket = False - $2.AfterDelegateDeclarationParameterComma = True - $2.BeforeSizeOfParentheses = True - $2.BeforeTypeOfParentheses = True - $2.SpacesAfterTypecast = True - $2.BlankLinesBeforeUsings = 1 - $2.inheritsSet = Mono - $2.inheritsScope = text/x-csharp - $2.scope = text/x-csharp - $0.TextStylePolicy = $3 - $3.FileWidth = 120 - $3.TabsToSpaces = False - $3.EolMarker = Unix - $3.inheritsSet = VisualStudio - $3.inheritsScope = text/plain - $3.scope = text/plain - EndGlobalSection -EndGlobal diff --git a/MailKit.Net45.sln b/MailKit.Net45.sln deleted file mode 100644 index ed17f92591..0000000000 --- a/MailKit.Net45.sln +++ /dev/null @@ -1,66 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 2012 -VisualStudioVersion = 12.0.31101.0 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MailKit.Net45", "MailKit\MailKit.Net45.csproj", "{7264D469-A390-4C10-9C87-DAA37EDD3C1D}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MimeKit.Net45", "submodules\MimeKit\MimeKit\MimeKit.Net45.csproj", "{D5F54A4F-D84B-430F-9271-F7861E285B3E}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests", "UnitTests\UnitTests.csproj", "{637EC535-3921-4A7A-8CB4-00A5AB18FAA2}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{BB3237C7-E19C-4232-B875-6658ABDD184A}" - ProjectSection(SolutionItems) = preProject - .nuget\packages.config = .nuget\packages.config - EndProjectSection -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {637EC535-3921-4A7A-8CB4-00A5AB18FAA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {637EC535-3921-4A7A-8CB4-00A5AB18FAA2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {637EC535-3921-4A7A-8CB4-00A5AB18FAA2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {637EC535-3921-4A7A-8CB4-00A5AB18FAA2}.Release|Any CPU.Build.0 = Release|Any CPU - {7264D469-A390-4C10-9C87-DAA37EDD3C1D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7264D469-A390-4C10-9C87-DAA37EDD3C1D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7264D469-A390-4C10-9C87-DAA37EDD3C1D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7264D469-A390-4C10-9C87-DAA37EDD3C1D}.Release|Any CPU.Build.0 = Release|Any CPU - {D5F54A4F-D84B-430F-9271-F7861E285B3E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D5F54A4F-D84B-430F-9271-F7861E285B3E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D5F54A4F-D84B-430F-9271-F7861E285B3E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D5F54A4F-D84B-430F-9271-F7861E285B3E}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - EndGlobalSection - GlobalSection(MonoDevelopProperties) = preSolution - StartupItem = MailKit\MailKit.Net45.csproj - Policies = $0 - $0.TextStylePolicy = $1 - $1.inheritsSet = null - $1.scope = text/x-csharp - $0.CSharpFormattingPolicy = $2 - $2.AlignEmbeddedUsingStatements = False - $2.AlignEmbeddedIfStatements = False - $2.NamespaceBraceStyle = EndOfLine - $2.StructBraceStyle = EndOfLine - $2.EnumBraceStyle = EndOfLine - $2.BeforeIndexerDeclarationBracket = False - $2.AfterDelegateDeclarationParameterComma = True - $2.BeforeSizeOfParentheses = True - $2.BeforeTypeOfParentheses = True - $2.SpacesAfterTypecast = True - $2.BlankLinesBeforeUsings = 1 - $2.inheritsSet = Mono - $2.inheritsScope = text/x-csharp - $2.scope = text/x-csharp - $0.StandardHeader = $3 - $3.Text = @\r\n${FileName}\n \nAuthor: ${AuthorName} <${AuthorEmail}>\n\nCopyright (c) ${Year} ${CopyrightHolder}\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the "Software"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n - $3.IncludeInNewFiles = True - EndGlobalSection -EndGlobal diff --git a/MailKit.sln b/MailKit.sln index 87192bec59..2270597fad 100644 --- a/MailKit.sln +++ b/MailKit.sln @@ -1,41 +1,18 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26228.9 +# Visual Studio Version 17 +VisualStudioVersion = 17.2.32516.85 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MailKit.Net45", "MailKit\MailKit.Net45.csproj", "{7264D469-A390-4C10-9C87-DAA37EDD3C1D}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{D5001AA9-4C61-475F-8EA3-4C15949D849F}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MimeKit.Net45", "submodules\MimeKit\MimeKit\MimeKit.Net45.csproj", "{D5F54A4F-D84B-430F-9271-F7861E285B3E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MimeKit", "submodules\MimeKit\MimeKit\MimeKit.csproj", "{B0E5B7C4-710E-4DDE-9C00-1234844ADA76}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MailKit.Net40", "MailKit\MailKit.Net40.csproj", "{DB3A2478-4742-452B-80C1-F672B64285AD}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MailKit", "MailKit\MailKit.csproj", "{12F096E4-8CDC-4D5B-87B8-8AD71A3B5BED}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MimeKit.Net40", "submodules\MimeKit\MimeKit\MimeKit.Net40.csproj", "{C909FC86-6084-41E5-B99C-DCDF2A5B7F82}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mono.Data.Sqlite", "submodules\MimeKit\Mono.Data.Sqlite\Mono.Data.Sqlite.csproj", "{F26434C1-BA3D-41FB-B560-C009CB72B1B6}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BouncyCastle.Android", "submodules\MimeKit\submodules\bc-csharp\crypto\BouncyCastle.Android.csproj", "{A0D302CB-8866-4AB1-98B9-F0772EABF5DF}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MailKit.Android", "MailKit\MailKit.Android.csproj", "{9BF818C3-C20E-4EFB-9426-F0A6C2EDC627}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MimeKit.Android", "submodules\MimeKit\MimeKit\MimeKit.Android.csproj", "{004B4019-62B7-4A15-AF2C-C20968845C46}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MailKit.iOS", "MailKit\MailKit.iOS.csproj", "{60B5D72B-8219-48B6-B688-AD0FE284A96A}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MimeKit.iOS", "submodules\MimeKit\MimeKit\MimeKit.iOS.csproj", "{4C1288AD-12C8-4BF7-AED7-6C4DC539C856}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BouncyCastle.iOS", "submodules\MimeKit\submodules\bc-csharp\crypto\BouncyCastle.iOS.csproj", "{0249241C-205E-4AC0-828B-90F822359B9E}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests", "UnitTests\UnitTests.csproj", "{637EC535-3921-4A7A-8CB4-00A5AB18FAA2}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MimeKit.NetStandard", "submodules\MimeKit\MimeKit\MimeKit.NetStandard.csproj", "{E8667DCE-A5BB-4D30-9815-FC8959E447F5}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MailKit.NetStandard", "MailKit\MailKit.NetStandard.csproj", "{507D2CF2-55FF-463F-8513-778442EBD251}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Portable.Text.Encoding.WindowsUniversal81", "submodules\MimeKit\submodules\Portable.Text.Encoding\Portable.Text.Encoding\Portable.Text.Encoding.WindowsUniversal81.csproj", "{B76A64F9-B00E-4243-AE89-5D024CA3B436}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MimeKit.WindowsUniversal81", "submodules\MimeKit\MimeKit\MimeKit.WindowsUniversal81.csproj", "{D9906B8C-7BBD-4CCE-AC7C-E9BCA020D20C}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MailKit.WindowsUniversal81", "MailKit\MailKit.WindowsUniversal81.csproj", "{5C20EB98-8084-41E7-952A-F297C0AAC916}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTests", "UnitTests\UnitTests.csproj", "{1B670279-AEA7-4D9B-A854-CB4CC177B277}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -43,101 +20,23 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {7264D469-A390-4C10-9C87-DAA37EDD3C1D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7264D469-A390-4C10-9C87-DAA37EDD3C1D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7264D469-A390-4C10-9C87-DAA37EDD3C1D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7264D469-A390-4C10-9C87-DAA37EDD3C1D}.Release|Any CPU.Build.0 = Release|Any CPU - {D5F54A4F-D84B-430F-9271-F7861E285B3E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D5F54A4F-D84B-430F-9271-F7861E285B3E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D5F54A4F-D84B-430F-9271-F7861E285B3E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D5F54A4F-D84B-430F-9271-F7861E285B3E}.Release|Any CPU.Build.0 = Release|Any CPU - {DB3A2478-4742-452B-80C1-F672B64285AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DB3A2478-4742-452B-80C1-F672B64285AD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DB3A2478-4742-452B-80C1-F672B64285AD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DB3A2478-4742-452B-80C1-F672B64285AD}.Release|Any CPU.Build.0 = Release|Any CPU - {C909FC86-6084-41E5-B99C-DCDF2A5B7F82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C909FC86-6084-41E5-B99C-DCDF2A5B7F82}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C909FC86-6084-41E5-B99C-DCDF2A5B7F82}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C909FC86-6084-41E5-B99C-DCDF2A5B7F82}.Release|Any CPU.Build.0 = Release|Any CPU - {F26434C1-BA3D-41FB-B560-C009CB72B1B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F26434C1-BA3D-41FB-B560-C009CB72B1B6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F26434C1-BA3D-41FB-B560-C009CB72B1B6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F26434C1-BA3D-41FB-B560-C009CB72B1B6}.Release|Any CPU.Build.0 = Release|Any CPU - {A0D302CB-8866-4AB1-98B9-F0772EABF5DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A0D302CB-8866-4AB1-98B9-F0772EABF5DF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A0D302CB-8866-4AB1-98B9-F0772EABF5DF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A0D302CB-8866-4AB1-98B9-F0772EABF5DF}.Release|Any CPU.Build.0 = Release|Any CPU - {9BF818C3-C20E-4EFB-9426-F0A6C2EDC627}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9BF818C3-C20E-4EFB-9426-F0A6C2EDC627}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9BF818C3-C20E-4EFB-9426-F0A6C2EDC627}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9BF818C3-C20E-4EFB-9426-F0A6C2EDC627}.Release|Any CPU.Build.0 = Release|Any CPU - {004B4019-62B7-4A15-AF2C-C20968845C46}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {004B4019-62B7-4A15-AF2C-C20968845C46}.Debug|Any CPU.Build.0 = Debug|Any CPU - {004B4019-62B7-4A15-AF2C-C20968845C46}.Release|Any CPU.ActiveCfg = Release|Any CPU - {004B4019-62B7-4A15-AF2C-C20968845C46}.Release|Any CPU.Build.0 = Release|Any CPU - {60B5D72B-8219-48B6-B688-AD0FE284A96A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {60B5D72B-8219-48B6-B688-AD0FE284A96A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {60B5D72B-8219-48B6-B688-AD0FE284A96A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {60B5D72B-8219-48B6-B688-AD0FE284A96A}.Release|Any CPU.Build.0 = Release|Any CPU - {4C1288AD-12C8-4BF7-AED7-6C4DC539C856}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4C1288AD-12C8-4BF7-AED7-6C4DC539C856}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4C1288AD-12C8-4BF7-AED7-6C4DC539C856}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4C1288AD-12C8-4BF7-AED7-6C4DC539C856}.Release|Any CPU.Build.0 = Release|Any CPU - {0249241C-205E-4AC0-828B-90F822359B9E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0249241C-205E-4AC0-828B-90F822359B9E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0249241C-205E-4AC0-828B-90F822359B9E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0249241C-205E-4AC0-828B-90F822359B9E}.Release|Any CPU.Build.0 = Release|Any CPU - {637EC535-3921-4A7A-8CB4-00A5AB18FAA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {637EC535-3921-4A7A-8CB4-00A5AB18FAA2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {637EC535-3921-4A7A-8CB4-00A5AB18FAA2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {637EC535-3921-4A7A-8CB4-00A5AB18FAA2}.Release|Any CPU.Build.0 = Release|Any CPU - {E8667DCE-A5BB-4D30-9815-FC8959E447F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E8667DCE-A5BB-4D30-9815-FC8959E447F5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E8667DCE-A5BB-4D30-9815-FC8959E447F5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E8667DCE-A5BB-4D30-9815-FC8959E447F5}.Release|Any CPU.Build.0 = Release|Any CPU - {B76A64F9-B00E-4243-AE89-5D024CA3B436}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B76A64F9-B00E-4243-AE89-5D024CA3B436}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B76A64F9-B00E-4243-AE89-5D024CA3B436}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B76A64F9-B00E-4243-AE89-5D024CA3B436}.Release|Any CPU.Build.0 = Release|Any CPU - {D9906B8C-7BBD-4CCE-AC7C-E9BCA020D20C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D9906B8C-7BBD-4CCE-AC7C-E9BCA020D20C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D9906B8C-7BBD-4CCE-AC7C-E9BCA020D20C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D9906B8C-7BBD-4CCE-AC7C-E9BCA020D20C}.Release|Any CPU.Build.0 = Release|Any CPU - {5C20EB98-8084-41E7-952A-F297C0AAC916}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5C20EB98-8084-41E7-952A-F297C0AAC916}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5C20EB98-8084-41E7-952A-F297C0AAC916}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5C20EB98-8084-41E7-952A-F297C0AAC916}.Release|Any CPU.Build.0 = Release|Any CPU - {507D2CF2-55FF-463F-8513-778442EBD251}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {507D2CF2-55FF-463F-8513-778442EBD251}.Debug|Any CPU.Build.0 = Debug|Any CPU - {507D2CF2-55FF-463F-8513-778442EBD251}.Release|Any CPU.ActiveCfg = Release|Any CPU - {507D2CF2-55FF-463F-8513-778442EBD251}.Release|Any CPU.Build.0 = Release|Any CPU + {B0E5B7C4-710E-4DDE-9C00-1234844ADA76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B0E5B7C4-710E-4DDE-9C00-1234844ADA76}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B0E5B7C4-710E-4DDE-9C00-1234844ADA76}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B0E5B7C4-710E-4DDE-9C00-1234844ADA76}.Release|Any CPU.Build.0 = Release|Any CPU + {12F096E4-8CDC-4D5B-87B8-8AD71A3B5BED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {12F096E4-8CDC-4D5B-87B8-8AD71A3B5BED}.Debug|Any CPU.Build.0 = Debug|Any CPU + {12F096E4-8CDC-4D5B-87B8-8AD71A3B5BED}.Release|Any CPU.ActiveCfg = Release|Any CPU + {12F096E4-8CDC-4D5B-87B8-8AD71A3B5BED}.Release|Any CPU.Build.0 = Release|Any CPU + {1B670279-AEA7-4D9B-A854-CB4CC177B277}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1B670279-AEA7-4D9B-A854-CB4CC177B277}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1B670279-AEA7-4D9B-A854-CB4CC177B277}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1B670279-AEA7-4D9B-A854-CB4CC177B277}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection - GlobalSection(MonoDevelopProperties) = preSolution - StartupItem = MailKit\MailKit.csproj - Policies = $0 - $0.TextStylePolicy = $1 - $1.inheritsSet = null - $1.scope = text/x-csharp - $0.CSharpFormattingPolicy = $2 - $2.AlignEmbeddedUsingStatements = False - $2.AlignEmbeddedIfStatements = False - $2.NamespaceBraceStyle = EndOfLine - $2.StructBraceStyle = EndOfLine - $2.EnumBraceStyle = EndOfLine - $2.BeforeIndexerDeclarationBracket = False - $2.AfterDelegateDeclarationParameterComma = True - $2.BeforeSizeOfParentheses = True - $2.BeforeTypeOfParentheses = True - $2.SpacesAfterTypecast = True - $2.BlankLinesBeforeUsings = 1 - $2.inheritsSet = Mono - $2.inheritsScope = text/x-csharp - $2.scope = text/x-csharp - $0.StandardHeader = $3 - $3.Text = @\r\n${FileName}\n \nAuthor: ${AuthorName} <${AuthorEmail}>\n\nCopyright (c) ${Year} ${CopyrightHolder}\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the "Software"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n - $3.IncludeInNewFiles = True + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {80311676-045A-4523-8BD6-AEAD1F21474C} EndGlobalSection EndGlobal diff --git a/MailKit/AccessControl.cs b/MailKit/AccessControl.cs index 3e07bae13a..c0d0efa8c1 100644 --- a/MailKit/AccessControl.cs +++ b/MailKit/AccessControl.cs @@ -1,9 +1,9 @@ -// +// // AccessControl.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -50,9 +50,9 @@ public class AccessControl /// The identifier name. /// The access rights. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// public AccessControl (string name, IEnumerable rights) { @@ -73,9 +73,9 @@ public AccessControl (string name, IEnumerable rights) /// The identifier name. /// The access rights. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// public AccessControl (string name, string rights) { @@ -95,7 +95,7 @@ public AccessControl (string name, string rights) /// /// The identifier name. /// - /// is null. + /// is . /// public AccessControl (string name) { diff --git a/MailKit/AccessControlList.cs b/MailKit/AccessControlList.cs index 409c10c619..cf8f7fc21a 100644 --- a/MailKit/AccessControlList.cs +++ b/MailKit/AccessControlList.cs @@ -1,9 +1,9 @@ -// +// // AccessControlList.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -47,7 +47,7 @@ public class AccessControlList : List /// /// The list of access controls. /// - /// is null. + /// is . /// public AccessControlList (IEnumerable controls) : base (controls) { diff --git a/MailKit/AccessRight.cs b/MailKit/AccessRight.cs index 17d153ef3e..be3efbf71e 100644 --- a/MailKit/AccessRight.cs +++ b/MailKit/AccessRight.cs @@ -1,9 +1,9 @@ -// +// // AccessRight.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -37,7 +37,7 @@ namespace MailKit { /// see https://tools.ietf.org/html/rfc4314#section-2.1 /// /// - public struct AccessRight : IEquatable + public readonly struct AccessRight : IEquatable { /// /// The access right for folder lookups. @@ -154,8 +154,8 @@ public AccessRight (char right) /// Determines whether the specified is equal to the current . /// /// The to compare with the current . - /// true if the specified is equal to the current - /// ; otherwise, false. + /// if the specified is equal to the current + /// ; otherwise, . public bool Equals (AccessRight other) { return other.Right == Right; @@ -169,7 +169,7 @@ public bool Equals (AccessRight other) /// /// Determines whether two access rights are equal. /// - /// true if and are equal; otherwise, false. + /// if and are equal; otherwise, . /// The first access right to compare. /// The second access right to compare. public static bool operator == (AccessRight right1, AccessRight right2) @@ -183,7 +183,7 @@ public bool Equals (AccessRight other) /// /// Determines whether two access rights are not equal. /// - /// true if and are not equal; otherwise, false. + /// if and are not equal; otherwise, . /// The first access right to compare. /// The second access right to compare. public static bool operator != (AccessRight right1, AccessRight right2) @@ -198,11 +198,11 @@ public bool Equals (AccessRight other) /// Determines whether the specified is equal to the current . /// /// The to compare with the current . - /// true if the specified is equal to the current ; - /// otherwise, false. - public override bool Equals (object obj) + /// if the specified is equal to the current ; + /// otherwise, . + public override bool Equals (object? obj) { - return obj is AccessRight && ((AccessRight) obj).Right == Right; + return obj is AccessRight right && right.Right == Right; } /// diff --git a/MailKit/AccessRights.cs b/MailKit/AccessRights.cs index ea50eaed80..1ef3619f9f 100644 --- a/MailKit/AccessRights.cs +++ b/MailKit/AccessRights.cs @@ -1,9 +1,9 @@ -// +// // AccessRights.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -47,7 +47,7 @@ public class AccessRights : ICollection /// /// The access rights. /// - /// is null. + /// is . /// public AccessRights (IEnumerable rights) { @@ -62,7 +62,7 @@ public AccessRights (IEnumerable rights) /// /// The access rights. /// - /// is null. + /// is . /// public AccessRights (string rights) { @@ -96,7 +96,7 @@ public int Count { /// /// Gets whether or not this set of access rights is read only. /// - /// true if this collection is read only; otherwise, false. + /// if this collection is read only; otherwise, . public bool IsReadOnly { get { return false; } } @@ -119,7 +119,7 @@ void ICollection.Add (AccessRight right) /// /// Adds the specified access right if it is not already included. /// - /// true if the right was added; otherwise, false. + /// if the right was added; otherwise, . /// The access right. public bool Add (AccessRight right) { @@ -137,7 +137,7 @@ public bool Add (AccessRight right) /// /// Adds the right specified by the given character. /// - /// true if the right was added; otherwise, false. + /// if the right was added; otherwise, . /// The right. public bool Add (char right) { @@ -152,7 +152,7 @@ public bool Add (char right) /// /// The rights. /// - /// is null. + /// is . /// public void AddRange (string rights) { @@ -171,7 +171,7 @@ public void AddRange (string rights) /// /// The rights. /// - /// is null. + /// is . /// public void AddRange (IEnumerable rights) { @@ -199,7 +199,7 @@ public void Clear () /// /// Determines whether or not the set of access rights already contains the specified right /// - /// true if the specified right exists; otherwise false. + /// if the specified right exists; otherwise, . /// The access right. public bool Contains (AccessRight right) { @@ -216,7 +216,7 @@ public bool Contains (AccessRight right) /// The array. /// The array index. /// - /// is null. + /// is . /// /// /// is out of range. @@ -238,7 +238,7 @@ public void CopyTo (AccessRight[] array, int arrayIndex) /// /// Removes the specified access right. /// - /// true if the access right was removed; otherwise false. + /// if the access right was removed; otherwise, . /// The access right. public bool Remove (AccessRight right) { diff --git a/MailKit/AlertEventArgs.cs b/MailKit/AlertEventArgs.cs index b9b40e61ab..0d7e986045 100644 --- a/MailKit/AlertEventArgs.cs +++ b/MailKit/AlertEventArgs.cs @@ -1,9 +1,9 @@ -// +// // AlertEventArgs.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -45,7 +45,7 @@ public class AlertEventArgs : EventArgs /// /// The alert message. /// - /// is null. + /// is . /// public AlertEventArgs (string message) { diff --git a/MailKit/Annotation.cs b/MailKit/Annotation.cs new file mode 100644 index 0000000000..2bc3029eff --- /dev/null +++ b/MailKit/Annotation.cs @@ -0,0 +1,82 @@ +// +// Annotation.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Collections.Generic; + +namespace MailKit { + /// + /// An annotation. + /// + /// + /// An annotation. + /// For more information about annotations, see + /// rfc5257. + /// + public class Annotation + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The annotation entry. + /// + /// is . + /// + public Annotation (AnnotationEntry entry) + { + if (entry is null) + throw new ArgumentNullException (nameof (entry)); + + Properties = new Dictionary (); + Entry = entry; + } + + /// + /// Get the annotation tag. + /// + /// + /// Gets the annotation tag. + /// + /// The annotation tag. + public AnnotationEntry Entry { + get; private set; + } + + /// + /// Get the annotation properties. + /// + /// + /// Gets the annotation properties. + /// + /// The annotation properties. + public Dictionary Properties { + get; private set; + } + } +} diff --git a/MailKit/Security/Ntlm/NtlmAuthLevel.cs b/MailKit/AnnotationAccess.cs similarity index 55% rename from MailKit/Security/Ntlm/NtlmAuthLevel.cs rename to MailKit/AnnotationAccess.cs index c80cae4664..0c4b6a08ba 100644 --- a/MailKit/Security/Ntlm/NtlmAuthLevel.cs +++ b/MailKit/AnnotationAccess.cs @@ -1,10 +1,9 @@ +// +// AnnotationAccess.cs // -// NtlmAuthLevel.cs +// Author: Jeffrey Stedfast // -// Author: -// Martin Baulig -// -// Copyright (c) 2012 Xamarin Inc. (http://www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -23,29 +22,32 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. +// + +namespace MailKit { + /// + /// An annotation access level. + /// + /// + /// An annotation access level. + /// For more information about annotations, see + /// rfc5257. + /// + public enum AnnotationAccess + { + /// + /// Annotations are not supported. + /// + None, + + /// + /// Annotations are read-only. + /// + ReadOnly, -namespace MailKit.Security.Ntlm { - /* - * On Windows, this is controlled by a registry setting - * (http://msdn.microsoft.com/en-us/library/ms814176.aspx) - * - * This can be configured by setting the static - * Type3Message.DefaultAuthLevel property, the default value - * is LM_and_NTLM_and_try_NTLMv2_Session. - */ - enum NtlmAuthLevel { - /* Use LM and NTLM, never use NTLMv2 session security. */ - LM_and_NTLM, - - /* Use NTLMv2 session security if the server supports it, - * otherwise fall back to LM and NTLM. */ - LM_and_NTLM_and_try_NTLMv2_Session, - - /* Use NTLMv2 session security if the server supports it, - * otherwise fall back to NTLM. Never use LM. */ - NTLM_only, - - /* Use NTLMv2 only. */ - NTLMv2_only, + /// + /// Annotations are read-write. + /// + ReadWrite } } diff --git a/MailKit/AnnotationAttribute.cs b/MailKit/AnnotationAttribute.cs new file mode 100644 index 0000000000..a2654f243e --- /dev/null +++ b/MailKit/AnnotationAttribute.cs @@ -0,0 +1,257 @@ +// +// AnnotationAttribute.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MailKit { + /// + /// An annotation attribute. + /// + /// + /// An annotation attribute. + /// For more information about annotations, see + /// rfc5257. + /// + public class AnnotationAttribute : IEquatable + { + static readonly char[] Wildcards = { '*', '%' }; + + /// + /// The annotation value. + /// + /// + /// Used to get or set both the private and shared values of an annotation. + /// + public static readonly AnnotationAttribute Value = new AnnotationAttribute ("value", AnnotationScope.Both); + + /// + /// The shared annotation value. + /// + /// + /// Used to get or set the shared value of an annotation. + /// + public static readonly AnnotationAttribute SharedValue = new AnnotationAttribute ("value", AnnotationScope.Shared); + + /// + /// The private annotation value. + /// + /// + /// Used to get or set the private value of an annotation. + /// + public static readonly AnnotationAttribute PrivateValue = new AnnotationAttribute ("value", AnnotationScope.Private); + + /// + /// The size of an annotation value. + /// + /// + /// Used to get the size of the both the private and shared annotation values. + /// + public static readonly AnnotationAttribute Size = new AnnotationAttribute ("size", AnnotationScope.Both); + + /// + /// The size of a shared annotation value. + /// + /// + /// Used to get the size of a shared annotation value. + /// + public static readonly AnnotationAttribute SharedSize = new AnnotationAttribute ("size", AnnotationScope.Shared); + + /// + /// The size of a private annotation value. + /// + /// + /// Used to get the size of a private annotation value. + /// + public static readonly AnnotationAttribute PrivateSize = new AnnotationAttribute ("size", AnnotationScope.Private); + + AnnotationAttribute (string name, AnnotationScope scope) + { + switch (scope) { + case AnnotationScope.Shared: Specifier = string.Format ("{0}.shared", name); break; + case AnnotationScope.Private: Specifier = string.Format ("{0}.priv", name); break; + default: Specifier = name; break; + } + Scope = scope; + Name = name; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The annotation attribute specifier. + /// + /// is . + /// + /// + /// contains illegal characters. + /// + public AnnotationAttribute (string specifier) + { + if (specifier == null) + throw new ArgumentNullException (nameof (specifier)); + + if (specifier.Length == 0) + throw new ArgumentException ("Annotation attribute specifiers cannot be empty.", nameof (specifier)); + + // TODO: improve validation + if (specifier.IndexOfAny (Wildcards) != -1) + throw new ArgumentException ("Annotation attribute specifiers cannot contain '*' or '%'.", nameof (specifier)); + + Specifier = specifier; + + if (specifier.EndsWith (".shared", StringComparison.Ordinal)) { + Name = specifier.Substring (0, specifier.Length - ".shared".Length); + Scope = AnnotationScope.Shared; + } else if (specifier.EndsWith (".priv", StringComparison.Ordinal)) { + Name = specifier.Substring (0, specifier.Length - ".priv".Length); + Scope = AnnotationScope.Private; + } else { + Scope = AnnotationScope.Both; + Name = specifier; + } + } + + /// + /// Get the name of the annotation attribute. + /// + /// + /// Gets the name of the annotation attribute. + /// + /// The name of the annotation attribute. + public string Name { + get; private set; + } + + /// + /// Get the scope of the annotation attribute. + /// + /// + /// Gets the scope of the annotation attribute. + /// + /// The scope of the annotation attribute. + public AnnotationScope Scope { + get; private set; + } + + /// + /// Get the annotation attribute specifier. + /// + /// + /// Gets the annotation attribute specifier. + /// + /// The annotation attribute specifier. + public string Specifier { + get; private set; + } + + #region IEquatable implementation + + /// + /// Determines whether the specified is equal to the current . + /// + /// + /// Determines whether the specified is equal to the current . + /// + /// The to compare with the current . + /// if the specified is equal to the current + /// ; otherwise, . + public bool Equals (AnnotationAttribute? other) + { + return other?.Specifier == Specifier; + } + + #endregion + + /// + /// Determines whether two annotation attributes are equal. + /// + /// + /// Determines whether two annotation attributes are equal. + /// + /// if and are equal; otherwise, . + /// The first annotation attribute to compare. + /// The second annotation attribute to compare. + public static bool operator == (AnnotationAttribute attr1, AnnotationAttribute attr2) + { + return attr1?.Specifier == attr2?.Specifier; + } + + /// + /// Determines whether two annotation attributes are not equal. + /// + /// + /// Determines whether two annotation attributes are not equal. + /// + /// if and are not equal; otherwise, . + /// The first annotation attribute to compare. + /// The second annotation attribute to compare. + public static bool operator != (AnnotationAttribute attr1, AnnotationAttribute attr2) + { + return attr1?.Specifier != attr2?.Specifier; + } + + /// + /// Determine whether the specified is equal to the current . + /// + /// + /// Determines whether the specified is equal to the current . + /// + /// The to compare with the current . + /// if the specified is equal to the current + /// ; otherwise, . + public override bool Equals (object? obj) + { + return obj is AnnotationAttribute attribute && attribute.Specifier == Specifier; + } + + /// + /// Serves as a hash function for a object. + /// + /// + /// Serves as a hash function for a object. + /// + /// A hash code for this instance that is suitable for use in hashing algorithms and data structures such as a hash table. + public override int GetHashCode () + { + return Specifier.GetHashCode (); + } + + /// + /// Returns a that represents the current . + /// + /// + /// Returns a that represents the current . + /// + /// A that represents the current . + public override string ToString () + { + return Specifier; + } + } +} diff --git a/MailKit/AnnotationEntry.cs b/MailKit/AnnotationEntry.cs new file mode 100644 index 0000000000..d1f2a14f23 --- /dev/null +++ b/MailKit/AnnotationEntry.cs @@ -0,0 +1,526 @@ +// +// AnnotationEntry.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MailKit { + /// + /// An annotation entry. + /// + /// + /// An annotation entry. + /// For more information about annotations, see + /// rfc5257. + /// + public class AnnotationEntry : IEquatable + { + /// + /// An annotation entry for a comment on a message. + /// + /// + /// Used to get or set a comment on a message. + /// + public static readonly AnnotationEntry Comment = new AnnotationEntry ("/comment", AnnotationScope.Both); + + /// + /// An annotation entry for a private comment on a message. + /// + /// + /// Used to get or set a private comment on a message. + /// + public static readonly AnnotationEntry PrivateComment = new AnnotationEntry ("/comment", AnnotationScope.Private); + + /// + /// An annotation entry for a shared comment on a message. + /// + /// + /// Used to get or set a shared comment on a message. + /// + public static readonly AnnotationEntry SharedComment = new AnnotationEntry ("/comment", AnnotationScope.Shared); + + /// + /// An annotation entry for flags on a message. + /// + /// + /// Used to get or set flags on a message. + /// + public static readonly AnnotationEntry Flags = new AnnotationEntry ("/flags", AnnotationScope.Both); + + /// + /// An annotation entry for private flags on a message. + /// + /// + /// Used to get or set private flags on a message. + /// + public static readonly AnnotationEntry PrivateFlags = new AnnotationEntry ("/flags", AnnotationScope.Private); + + /// + /// Aa annotation entry for shared flags on a message. + /// + /// + /// Used to get or set shared flags on a message. + /// + public static readonly AnnotationEntry SharedFlags = new AnnotationEntry ("/flags", AnnotationScope.Shared); + + /// + /// An annotation entry for an alternate subject on a message. + /// + /// + /// Used to get or set an alternate subject on a message. + /// + public static readonly AnnotationEntry AltSubject = new AnnotationEntry ("/altsubject", AnnotationScope.Both); + + /// + /// An annotation entry for a private alternate subject on a message. + /// + /// + /// Used to get or set a private alternate subject on a message. + /// + public static readonly AnnotationEntry PrivateAltSubject = new AnnotationEntry ("/altsubject", AnnotationScope.Private); + + /// + /// An annotation entry for a shared alternate subject on a message. + /// + /// + /// Used to get or set a shared alternate subject on a message. + /// + public static readonly AnnotationEntry SharedAltSubject = new AnnotationEntry ("/altsubject", AnnotationScope.Shared); + + static void ValidatePath (string path) + { + if (path == null) + throw new ArgumentNullException (nameof (path)); + + if (path.Length == 0) + throw new ArgumentException ("Annotation entry paths cannot be empty.", nameof (path)); + + if (path[0] != '/' && path[0] != '*' && path[0] != '%') + throw new ArgumentException ("Annotation entry paths must begin with '/'.", nameof (path)); + + if (path.Length > 1 && path[1] >= '0' && path[1] <= '9') + throw new ArgumentException ("Annotation entry paths must not include a part-specifier.", nameof (path)); + + if (path == "*" || path == "%") + return; + + char pc = path[0]; + + for (int i = 1; i < path.Length; i++) { + char c = path[i]; + + if (c > 127) + throw new ArgumentException ($"Invalid character in annotation entry path: '{c}'.", nameof (path)); + + if (c >= '0' && c <= '9' && pc == '/') + throw new ArgumentException ("Invalid annotation entry path.", nameof (path)); + + if ((pc == '/' || pc == '.') && (c == '/' || c == '.')) + throw new ArgumentException ("Invalid annotation entry path.", nameof (path)); + + pc = c; + } + + int endIndex = path.Length - 1; + + if (path[endIndex] == '/') + throw new ArgumentException ("Annotation entry paths must not end with '/'.", nameof (path)); + + if (path[endIndex] == '.') + throw new ArgumentException ("Annotation entry paths must not end with '.'.", nameof (path)); + } + + static void ValidatePartSpecifier (string partSpecifier) + { + if (partSpecifier == null) + throw new ArgumentNullException (nameof (partSpecifier)); + + char pc = '\0'; + + for (int i = 0; i < partSpecifier.Length; i++) { + char c = partSpecifier[i]; + + if (!((c >= '0' && c <= '9') || c == '.') || (c == '.' && (pc == '.' || pc == '\0'))) + throw new ArgumentException ("Invalid part-specifier.", nameof (partSpecifier)); + + pc = c; + } + + if (pc == '.') + throw new ArgumentException ("Invalid part-specifier.", nameof (partSpecifier)); + } + + AnnotationEntry (string? partSpecifier, string entry, string path, AnnotationScope scope) + { + PartSpecifier = partSpecifier; + Entry = entry; + Path = path; + Scope = scope; + } + + /// + /// Initializes a new instance of the struct. + /// + /// + /// Creates a new . + /// + /// The annotation entry path. + /// The scope of the annotation. + /// + /// is . + /// + /// + /// is invalid. + /// + public AnnotationEntry (string path, AnnotationScope scope = AnnotationScope.Both) + { + ValidatePath (path); + + switch (scope) { + case AnnotationScope.Private: Entry = path + ".priv"; break; + case AnnotationScope.Shared: Entry = path + ".shared"; break; + default: Entry = path; break; + } + + PartSpecifier = null; + Path = path; + Scope = scope; + } + + /// + /// Initializes a new instance of the struct. + /// + /// + /// Creates a new for an individual body part of a message. + /// + /// The part-specifier of the body part of the message. + /// The annotation entry path. + /// The scope of the annotation. + /// + /// is . + /// -or- + /// is . + /// + /// + /// is invalid. + /// -or- + /// is invalid. + /// + public AnnotationEntry (string partSpecifier, string path, AnnotationScope scope = AnnotationScope.Both) + { + ValidatePartSpecifier (partSpecifier); + ValidatePath (path); + + switch (scope) { + case AnnotationScope.Private: Entry = string.Format ("/{0}{1}.priv", partSpecifier, path); break; + case AnnotationScope.Shared: Entry = string.Format ("/{0}{1}.shared", partSpecifier, path); break; + default: Entry = string.Format ("/{0}{1}", partSpecifier, path); break; + } + + PartSpecifier = partSpecifier; + Path = path; + Scope = scope; + } + + /// + /// Initializes a new instance of the struct. + /// + /// + /// Creates a new for an individual body part of a message. + /// + /// The body part of the message. + /// The annotation entry path. + /// The scope of the annotation. + /// + /// is . + /// -or- + /// is . + /// + /// + /// is invalid. + /// + public AnnotationEntry (BodyPart part, string path, AnnotationScope scope = AnnotationScope.Both) + { + if (part == null) + throw new ArgumentNullException (nameof (part)); + + ValidatePath (path); + + switch (scope) { + case AnnotationScope.Private: Entry = string.Format ("/{0}{1}.priv", part.PartSpecifier, path); break; + case AnnotationScope.Shared: Entry = string.Format ("/{0}{1}.shared", part.PartSpecifier, path); break; + default: Entry = string.Format ("/{0}{1}", part.PartSpecifier, path); break; + } + + PartSpecifier = part.PartSpecifier; + Path = path; + Scope = scope; + } + + /// + /// Get the annotation entry specifier. + /// + /// + /// Gets the annotation entry specifier. + /// + /// The annotation entry specifier. + public string Entry { + get; private set; + } + + /// + /// Get the part-specifier component of the annotation entry. + /// + /// + /// Gets the part-specifier component of the annotation entry. + /// + /// The part-specifier. + public string? PartSpecifier { + get; private set; + } + + /// + /// Get the path component of the annotation entry. + /// + /// + /// Gets the path component of the annotation entry. + /// + /// The path. + public string Path { + get; private set; + } + + /// + /// Get the scope of the annotation. + /// + /// + /// Gets the scope of the annotation. + /// + /// The scope. + public AnnotationScope Scope { + get; private set; + } + + #region IEquatable implementation + + /// + /// Determines whether the specified is equal to the current . + /// + /// + /// Determines whether the specified is equal to the current . + /// + /// The to compare with the current . + /// if the specified is equal to the current + /// ; otherwise, . + public bool Equals (AnnotationEntry? other) + { + return other?.Entry == Entry; + } + + #endregion + + /// + /// Determines whether two annotation entries are equal. + /// + /// + /// Determines whether two annotation entries are equal. + /// + /// if and are equal; otherwise, . + /// The first annotation entry to compare. + /// The second annotation entry to compare. + public static bool operator == (AnnotationEntry entry1, AnnotationEntry entry2) + { + return entry1?.Entry == entry2?.Entry; + } + + /// + /// Determines whether two annotation entries are not equal. + /// + /// + /// Determines whether two annotation entries are not equal. + /// + /// if and are not equal; otherwise, . + /// The first annotation entry to compare. + /// The second annotation entry to compare. + public static bool operator != (AnnotationEntry entry1, AnnotationEntry entry2) + { + return entry1?.Entry != entry2?.Entry; + } + + /// + /// Determine whether the specified is equal to the current . + /// + /// + /// Determines whether the specified is equal to the current . + /// + /// The to compare with the current . + /// if the specified is equal to the current + /// ; otherwise, . + public override bool Equals (object? obj) + { + return obj is AnnotationEntry entry && entry.Entry == Entry; + } + + /// + /// Serves as a hash function for a object. + /// + /// + /// Serves as a hash function for a object. + /// + /// A hash code for this instance that is suitable for use in hashing algorithms and data structures such as a hash table. + public override int GetHashCode () + { + return Entry.GetHashCode (); + } + + /// + /// Returns a that represents the current . + /// + /// + /// Returns a that represents the current . + /// + /// A that represents the current . + public override string ToString () + { + return Entry; + } + + /// + /// Parse an annotation entry. + /// + /// + /// Parses an annotation entry. + /// + /// The annotation entry. + /// The parsed annotation entry. + /// + /// is . + /// + /// + /// does not conform to the annotation entry syntax. + /// + public static AnnotationEntry Parse (string entry) + { + if (entry == null) + throw new ArgumentNullException (nameof (entry)); + + if (entry.Length == 0) + throw new FormatException ("An annotation entry cannot be empty."); + + if (entry[0] != '/' && entry[0] != '*' && entry[0] != '%') + throw new FormatException ("An annotation entry must begin with a '/' character."); + + var scope = AnnotationScope.Both; + int startIndex = 0, endIndex; + string? partSpecifier = null; + var component = 0; + var pc = entry[0]; + string path; + + for (int i = 1; i < entry.Length; i++) { + char c = entry[i]; + + if (c >= '0' && c <= '9' && pc == '/') { + if (component > 0) + throw new FormatException ("Invalid annotation entry."); + + startIndex = i; + endIndex = i + 1; + pc = c; + + while (endIndex < entry.Length) { + c = entry[endIndex]; + + if (c == '/') { + if (pc == '.') + throw new FormatException ("Invalid part-specifier in annotation entry."); + + break; + } + + if (!(c >= '0' && c <= '9') && c != '.') + throw new FormatException ($"Invalid character in part-specifier: '{c}'."); + + if (c == '.' && pc == '.') + throw new FormatException ("Invalid part-specifier in annotation entry."); + + endIndex++; + pc = c; + } + + if (endIndex >= entry.Length) + throw new FormatException ("Incomplete part-specifier in annotation entry."); + + partSpecifier = entry.Substring (startIndex, endIndex - startIndex); + i = startIndex = endIndex; + component++; + } else if (c == '/' || c == '.') { + if (pc == '/' || pc == '.') + throw new FormatException ("Invalid annotation entry path."); + + if (c == '/') + component++; + } else if (c > 127) { + throw new FormatException ($"Invalid character in annotation entry path: '{c}'."); + } + + pc = c; + } + + if (pc == '/' || pc == '.') + throw new FormatException ("Invalid annotation entry path."); + + if (entry.EndsWith (".shared", StringComparison.Ordinal)) { + endIndex = entry.Length - ".shared".Length; + scope = AnnotationScope.Shared; + } else if (entry.EndsWith (".priv", StringComparison.Ordinal)) { + endIndex = entry.Length - ".priv".Length; + scope = AnnotationScope.Private; + } else { + endIndex = entry.Length; + } + + path = entry.Substring (startIndex, endIndex - startIndex); + + return new AnnotationEntry (partSpecifier, entry, path, scope); + } + + internal static AnnotationEntry Create (string entry) + { + switch (entry) { + case "/comment": return Comment; + case "/comment.priv": return PrivateComment; + case "/comment.shared": return SharedComment; + case "/flags": return Flags; + case "/flags.priv": return PrivateFlags; + case "/flags.shared": return SharedFlags; + case "/altsubject": return AltSubject; + case "/altsubject.priv": return PrivateAltSubject; + case "/altsubject.shared": return SharedAltSubject; + default: return Parse (entry); + } + } + } +} diff --git a/MailKit/MessagesArrivedEventArgs.cs b/MailKit/AnnotationScope.cs similarity index 61% rename from MailKit/MessagesArrivedEventArgs.cs rename to MailKit/AnnotationScope.cs index 82fafbeee8..d2c84fbb27 100644 --- a/MailKit/MessagesArrivedEventArgs.cs +++ b/MailKit/AnnotationScope.cs @@ -1,9 +1,9 @@ -// -// MessagesArrivedEventArgs.cs +// +// AnnotationScope.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -28,34 +28,34 @@ namespace MailKit { /// - /// Event args used when messages arrive in a folder. + /// The scope of an annotation. /// /// - /// Event args used when messages arrive in a folder. + /// Represents the scope of an annotation. + /// For more information about annotations, see + /// rfc5257. /// - public class MessagesArrivedEventArgs : EventArgs + [Flags] + public enum AnnotationScope { /// - /// Initializes a new instance of the class. + /// No scopes. + /// + None, + + /// + /// The private annotation scope. + /// + Private, + + /// + /// The shared annotation scope. /// - /// - /// Creates a new . - /// - /// The number of messages that just arrived. - public MessagesArrivedEventArgs (int count) - { - Count = count; - } + Shared, /// - /// Get the number of messages that just arrived in the folder. + /// Both private and shared scopes. /// - /// - /// Gets the number of messages that just arrived in the folder. - /// - /// The count. - public int Count { - get; private set; - } + Both = Private | Shared } } diff --git a/MailKit/AnnotationsChangedEventArgs.cs b/MailKit/AnnotationsChangedEventArgs.cs new file mode 100644 index 0000000000..25144ad860 --- /dev/null +++ b/MailKit/AnnotationsChangedEventArgs.cs @@ -0,0 +1,82 @@ +// +// AnnotationsChangedEventArgs.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Linq; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace MailKit { + /// + /// Event args used when an annotation changes. + /// + /// + /// Event args used when an annotation changes. + /// + public class AnnotationsChangedEventArgs : MessageEventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The message index. + /// The annotations that changed. + /// + /// is . + /// + public AnnotationsChangedEventArgs (int index, IEnumerable annotations) : base (index) + { + if (annotations == null) + throw new ArgumentNullException (nameof (annotations)); + + Annotations = new ReadOnlyCollection (annotations.ToArray ()); + } + + /// + /// Get the annotations that changed. + /// + /// + /// Gets the annotations that changed. + /// + /// The annotation. + public IList Annotations { + get; internal set; + } + + /// + /// Gets the updated mod-sequence value of the message, if available. + /// + /// + /// Gets the updated mod-sequence value of the message, if available. + /// + /// The mod-sequence value. + public ulong? ModSeq { + get; internal set; + } + } +} diff --git a/MailKit/AppendRequest.cs b/MailKit/AppendRequest.cs new file mode 100644 index 0000000000..af3d9457a4 --- /dev/null +++ b/MailKit/AppendRequest.cs @@ -0,0 +1,210 @@ +// +// AppendRequest.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Collections.Generic; + +using MimeKit; + +namespace MailKit { + /// + /// A request for appending a message to a folder. + /// + /// + /// A request for appending a message to a folder. + /// + public class AppendRequest : IAppendRequest + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The message. + /// The message flags. + /// + /// is . + /// + public AppendRequest (MimeMessage message, MessageFlags flags = MessageFlags.None) + { + if (message == null) + throw new ArgumentNullException (nameof (message)); + + Message = message; + Flags = flags; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The message. + /// The message flags. + /// The message keywords. + /// + /// is . + /// -or- + /// is . + /// + public AppendRequest (MimeMessage message, MessageFlags flags, IEnumerable keywords) + { + if (message == null) + throw new ArgumentNullException (nameof (message)); + + if (keywords == null) + throw new ArgumentNullException (nameof (keywords)); + + Keywords = keywords as ISet ?? new HashSet (keywords); + Message = message; + Flags = flags; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The message. + /// The message flags. + /// The internal date of the message. + /// + /// is . + /// + public AppendRequest (MimeMessage message, MessageFlags flags, DateTimeOffset internalDate) + { + if (message == null) + throw new ArgumentNullException (nameof (message)); + + Message = message; + Flags = flags; + InternalDate = internalDate; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The message. + /// The message flags. + /// The message keywords. + /// The internal date of the message. + /// + /// is . + /// -or- + /// is . + /// + public AppendRequest (MimeMessage message, MessageFlags flags, IEnumerable keywords, DateTimeOffset internalDate) + { + if (message == null) + throw new ArgumentNullException (nameof (message)); + + if (keywords == null) + throw new ArgumentNullException (nameof (keywords)); + + Keywords = keywords as ISet ?? new HashSet (keywords); + Message = message; + Flags = flags; + InternalDate = internalDate; + } + + /// + /// Get the message that should be appended to the folder. + /// + /// + /// Gets the message that should be appended to the folder. + /// + /// The message. + public MimeMessage Message { + get; private set; + } + + /// + /// Get or set the message flags that should be set on the message. + /// + /// + /// Gets or sets the message flags that should be set on the message. + /// + /// The message flags. + public MessageFlags Flags { + get; set; + } + + /// + /// Get or set the keywords that should be set on the message. + /// + /// + /// Gets or sets the keywords that should be set on the message. + /// + /// The keywords. + public ISet? Keywords { + get; set; + } + + /// + /// Get or set the timestamp that should be used by folder as the . + /// + /// + /// Gets or sets the timestamp that should be used by folder as the . + /// + /// The date and time to use for the INTERNALDATE or if it should be left up to the folder to decide. + public DateTimeOffset? InternalDate { + get; set; + } + + /// + /// Get or set the list of annotations that should be set on the message. + /// + /// + /// Gets or sets the list of annotations that should be set on the message. + /// + /// This feature is not supported by all folders. + /// Use with the enum value + /// to determine if this feature is supported. + /// + /// + /// The list of annotations. + public IList? Annotations { + get; set; + } + + /// + /// Get or set the transfer progress reporting mechanism. + /// + /// + /// Gets or sets the transfer progress reporting mechanism. + /// + /// The transfer progress mechanism. + public ITransferProgress? TransferProgress { + get; set; + } + } +} diff --git a/MailKit/AuthenticatedEventArgs.cs b/MailKit/AuthenticatedEventArgs.cs index ee91f04624..e458464474 100644 --- a/MailKit/AuthenticatedEventArgs.cs +++ b/MailKit/AuthenticatedEventArgs.cs @@ -1,9 +1,9 @@ -// +// // AuthenticatedEventArgs.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -43,8 +43,14 @@ public class AuthenticatedEventArgs : EventArgs /// Creates a new . /// /// The free-form text. + /// + /// is . + /// public AuthenticatedEventArgs (string message) { + if (message == null) + throw new ArgumentNullException (nameof (message)); + Message = message; } diff --git a/MailKit/BodyPart.cs b/MailKit/BodyPart.cs index 3b3ae0e02e..f66785c04b 100644 --- a/MailKit/BodyPart.cs +++ b/MailKit/BodyPart.cs @@ -1,9 +1,9 @@ -// +// // BodyPart.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -26,7 +26,9 @@ using System; using System.Text; +using System.Globalization; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using MimeKit; using MimeKit.Utils; @@ -41,7 +43,7 @@ namespace MailKit { /// . /// /// - /// + /// /// public abstract class BodyPart { @@ -51,8 +53,36 @@ public abstract class BodyPart /// /// Creates a new . /// + [Obsolete ("Use BodyPart (ContentType, string) instead.")] protected BodyPart () { + ContentType = new ContentType ("application", "octet-stream"); + PartSpecifier = string.Empty; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The content type. + /// The part specifier. + /// + /// is . + /// -or- + /// is . + /// + protected BodyPart (ContentType contentType, string partSpecifier) + { + if (contentType == null) + throw new ArgumentNullException (nameof (contentType)); + + if (partSpecifier == null) + throw new ArgumentNullException (nameof (partSpecifier)); + + ContentType = contentType; + PartSpecifier = partSpecifier; } /// @@ -73,7 +103,7 @@ public ContentType ContentType { /// Gets the part specifier. /// /// - /// + /// /// /// The part specifier. public string PartSpecifier { @@ -93,38 +123,32 @@ public string PartSpecifier { /// /// The visitor. /// - /// is null. + /// is . /// - public virtual void Accept (BodyPartVisitor visitor) - { - if (visitor == null) - throw new ArgumentNullException (nameof (visitor)); - - visitor.VisitBodyPart (this); - } + public abstract void Accept (BodyPartVisitor visitor); internal static void Encode (StringBuilder builder, uint value) { - builder.Append (value.ToString ()); + builder.Append (value.ToString (CultureInfo.InvariantCulture)); } - internal static void Encode (StringBuilder builder, string value) + internal static void Encode (StringBuilder builder, string? value) { if (value != null) - builder.Append (MimeUtils.Quote (value)); + MimeUtils.AppendQuoted (builder, value); else builder.Append ("NIL"); } - internal static void Encode (StringBuilder builder, Uri location) + internal static void Encode (StringBuilder builder, Uri? location) { if (location != null) - builder.Append (MimeUtils.Quote (location.ToString ())); + MimeUtils.AppendQuoted (builder, location.ToString ()); else builder.Append ("NIL"); } - internal static void Encode (StringBuilder builder, string[] values) + internal static void Encode (StringBuilder builder, string[]? values) { if (values == null || values.Length == 0) { builder.Append ("NIL"); @@ -164,7 +188,7 @@ internal static void Encode (StringBuilder builder, IList parameters) builder.Append (')'); } - internal static void Encode (StringBuilder builder, ContentDisposition disposition) + internal static void Encode (StringBuilder builder, ContentDisposition? disposition) { if (disposition == null) { builder.Append ("NIL"); @@ -202,7 +226,7 @@ internal static void Encode (StringBuilder builder, BodyPartCollection parts) } } - internal static void Encode (StringBuilder builder, Envelope envelope) + internal static void Encode (StringBuilder builder, Envelope? envelope) { if (envelope == null) { builder.Append ("NIL"); @@ -212,7 +236,7 @@ internal static void Encode (StringBuilder builder, Envelope envelope) envelope.Encode (builder); } - internal static void Encode (StringBuilder builder, BodyPart body) + internal static void Encode (StringBuilder builder, BodyPart? body) { if (body == null) { builder.Append ("NIL"); @@ -253,6 +277,11 @@ public override string ToString () return builder.ToString (); } + static bool IsNIL (string text, int index) + { + return string.Compare (text, index, "NIL", 0, 3, StringComparison.Ordinal) == 0; + } + static bool TryParse (string text, ref int index, out uint value) { while (index < text.Length && text[index] == ' ') @@ -268,7 +297,7 @@ static bool TryParse (string text, ref int index, out uint value) return index > startIndex; } - static bool TryParse (string text, ref int index, out string nstring) + static bool TryParse (string text, ref int index, out string? nstring) { nstring = null; @@ -279,7 +308,7 @@ static bool TryParse (string text, ref int index, out string nstring) return false; if (text[index] != '"') { - if (index + 3 <= text.Length && text.Substring (index, 3) == "NIL") { + if (index + 3 <= text.Length && IsNIL (text, index)) { index += 3; return true; } @@ -316,7 +345,7 @@ static bool TryParse (string text, ref int index, out string nstring) return true; } - static bool TryParse (string text, ref int index, out string[] values) + static bool TryParse (string text, ref int index, out string[]? values) { values = null; @@ -327,7 +356,7 @@ static bool TryParse (string text, ref int index, out string[] values) return false; if (text[index] != '(') { - if (index + 3 <= text.Length && text.Substring (index, 3) == "NIL") { + if (index + 3 <= text.Length && IsNIL (text, index)) { index += 3; return true; } @@ -341,33 +370,32 @@ static bool TryParse (string text, ref int index, out string[] values) return false; var list = new List (); - string value; do { if (text[index] == ')') break; - if (!TryParse (text, ref index, out value)) + if (!TryParse (text, ref index, out string? value)) return false; - list.Add (value); + if (value != null) + list.Add (value); } while (index < text.Length); if (index >= text.Length || text[index] != ')') return false; + values = list.ToArray (); index++; return true; } - static bool TryParse (string text, ref int index, out Uri uri) + static bool TryParse (string text, ref int index, out Uri? uri) { - string nstring; - uri = null; - if (!TryParse (text, ref index, out nstring)) + if (!TryParse (text, ref index, out string? nstring)) return false; if (!string.IsNullOrEmpty (nstring)) { @@ -380,10 +408,8 @@ static bool TryParse (string text, ref int index, out Uri uri) return true; } - static bool TryParse (string text, ref int index, out IList parameters) + static bool TryParse (string text, ref int index, [NotNullWhen (true)] out IList? parameters) { - string name, value; - parameters = null; while (index < text.Length && text[index] == ' ') @@ -393,7 +419,7 @@ static bool TryParse (string text, ref int index, out IList parameter return false; if (text[index] != '(') { - if (index + 3 <= text.Length && text.Substring (index, 3) == "NIL") { + if (index + 3 <= text.Length && IsNIL (text, index)) { parameters = new List (); index += 3; return true; @@ -413,10 +439,10 @@ static bool TryParse (string text, ref int index, out IList parameter if (text[index] == ')') break; - if (!TryParse (text, ref index, out name)) + if (!TryParse (text, ref index, out string? name) || name == null) return false; - if (!TryParse (text, ref index, out value)) + if (!TryParse (text, ref index, out string? value) || value == null) return false; parameters.Add (new Parameter (name, value)); @@ -430,11 +456,8 @@ static bool TryParse (string text, ref int index, out IList parameter return true; } - static bool TryParse (string text, ref int index, out ContentDisposition disposition) + static bool TryParse (string text, ref int index, out ContentDisposition? disposition) { - IList parameters; - string value; - disposition = null; while (index < text.Length && text[index] == ' ') @@ -444,7 +467,7 @@ static bool TryParse (string text, ref int index, out ContentDisposition disposi return false; if (text[index] != '(') { - if (index + 3 <= text.Length && text.Substring (index, 3) == "NIL") { + if (index + 3 <= text.Length && IsNIL (text, index)) { index += 3; return true; } @@ -454,10 +477,10 @@ static bool TryParse (string text, ref int index, out ContentDisposition disposi index++; - if (!TryParse (text, ref index, out value)) + if (!TryParse (text, ref index, out string? value) || value == null) return false; - if (!TryParse (text, ref index, out parameters)) + if (!TryParse (text, ref index, out IList? parameters)) return false; if (index >= text.Length || text[index] != ')') @@ -473,10 +496,9 @@ static bool TryParse (string text, ref int index, out ContentDisposition disposi return true; } - static bool TryParse (string text, ref int index, bool multipart, out ContentType contentType) + static bool TryParse (string text, ref int index, bool multipart, [NotNullWhen (true)] out ContentType? contentType) { - IList parameters; - string type, subtype; + string? type, subtype; contentType = null; @@ -496,7 +518,7 @@ static bool TryParse (string text, ref int index, bool multipart, out ContentTyp if (!TryParse (text, ref index, out subtype)) return false; - if (!TryParse (text, ref index, out parameters)) + if (!TryParse (text, ref index, out IList? parameters)) return false; contentType = new ContentType (type ?? "application", subtype ?? "octet-stream"); @@ -507,18 +529,17 @@ static bool TryParse (string text, ref int index, bool multipart, out ContentTyp return true; } - static bool TryParse (string text, ref int index, string prefix, out IList children) + static bool TryParse (string text, ref int index, string prefix, [NotNullWhen (true)] out BodyPartCollection? bodyParts) { - BodyPart part; string path; int id = 1; - children = null; + bodyParts = null; if (index >= text.Length) return false; - children = new List (); + bodyParts = new BodyPartCollection (); do { if (text[index] != '(') @@ -526,27 +547,27 @@ static bool TryParse (string text, ref int index, string prefix, out IList= text.Length || text[index] != '(') { - if (index + 3 <= text.Length && text.Substring (index, 3) == "NIL") { + if (index + 3 <= text.Length && IsNIL (text, index)) { index += 3; return true; } @@ -567,21 +588,21 @@ static bool TryParse (string text, ref int index, string path, out BodyPart part if (index >= text.Length) return false; - if (text[index] == '(') { + if (text[index] == '(' || IsNIL (text, index)) { var prefix = path.Length > 0 ? path + "." : string.Empty; - var multipart = new BodyPartMultipart (); - IList children; + BodyPartCollection? bodyParts = null; - if (!TryParse (text, ref index, prefix, out children)) - return false; - - foreach (var child in children) - multipart.BodyParts.Add (child); + if (text[index] == '(') { + if (!TryParse (text, ref index, prefix, out bodyParts)) + return false; + } else { + index += "NIL".Length; + } if (!TryParse (text, ref index, true, out contentType)) return false; - multipart.ContentType = contentType; + var multipart = new BodyPartMultipart (contentType, path, bodyParts ?? new BodyPartCollection ()); if (!TryParse (text, ref index, out disposition)) return false; @@ -600,21 +621,20 @@ static bool TryParse (string text, ref int index, string path, out BodyPart part part = multipart; } else { - BodyPartMessage message = null; - BodyPartText txt = null; - BodyPartBasic basic; + BodyPartMessage? message = null; + BodyPartText? txt = null; + BodyPartBasic? basic; + string? nstring; if (!TryParse (text, ref index, false, out contentType)) return false; if (contentType.IsMimeType ("message", "rfc822")) - basic = message = new BodyPartMessage (); + basic = message = new BodyPartMessage (contentType, path); else if (contentType.IsMimeType ("text", "*")) - basic = txt = new BodyPartText (); + basic = txt = new BodyPartText (contentType, path); else - basic = new BodyPartBasic (); - - basic.ContentType = contentType; + basic = new BodyPartBasic (contentType, path); if (!TryParse (text, ref index, out nstring)) return false; @@ -631,7 +651,7 @@ static bool TryParse (string text, ref int index, string path, out BodyPart part basic.ContentTransferEncoding = nstring; - if (!TryParse (text, ref index, out number)) + if (!TryParse (text, ref index, out uint number)) return false; basic.Octets = number; @@ -657,15 +677,12 @@ static bool TryParse (string text, ref int index, string path, out BodyPart part basic.ContentLocation = location; if (message != null) { - Envelope envelope; - BodyPart body; - - if (!Envelope.TryParse (text, ref index, out envelope)) + if (!Envelope.TryParse (text, ref index, out Envelope? envelope)) return false; message.Envelope = envelope; - if (!TryParse (text, ref index, path, out body)) + if (!TryParse (text, ref index, path, out BodyPart? body)) return false; message.Body = body; @@ -684,8 +701,6 @@ static bool TryParse (string text, ref int index, string path, out BodyPart part part = basic; } - part.PartSpecifier = path; - if (index >= text.Length || text[index] != ')') return false; @@ -702,13 +717,13 @@ static bool TryParse (string text, ref int index, string path, out BodyPart part /// This syntax, while similar to IMAP's BODYSTRUCTURE syntax, is not completely /// compatible. /// - /// true, if the body part was successfully parsed, false otherwise. + /// if the body part was successfully parsed; otherwise, . /// The text to parse. /// The parsed body part. /// - /// is null. + /// is . /// - public static bool TryParse (string text, out BodyPart part) + public static bool TryParse (string text, out BodyPart? part) { if (text == null) throw new ArgumentNullException (nameof (text)); diff --git a/MailKit/BodyPartBasic.cs b/MailKit/BodyPartBasic.cs index 454f4da96c..50c1ced90a 100644 --- a/MailKit/BodyPartBasic.cs +++ b/MailKit/BodyPartBasic.cs @@ -1,9 +1,9 @@ -// +// // BodyPartBasic.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -38,7 +38,7 @@ namespace MailKit { /// message/rfc822 part, or a text part. /// /// - /// + /// /// public class BodyPartBasic : BodyPart { @@ -48,7 +48,25 @@ public class BodyPartBasic : BodyPart /// /// Creates a new . /// - public BodyPartBasic () + [Obsolete ("Use BodyPartBasic (ContentType, string) instead.")] + public BodyPartBasic () : base () + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The content type. + /// The part specifier. + /// + /// is . + /// -or- + /// is . + /// + public BodyPartBasic (ContentType contentType, string partSpecifier) : base (contentType, partSpecifier) { } @@ -59,7 +77,7 @@ public BodyPartBasic () /// Gets the Content-Id of the body part, if available. /// /// The content identifier. - public string ContentId { + public string? ContentId { get; set; } @@ -70,7 +88,7 @@ public string ContentId { /// Gets the Content-Description of the body part, if available. /// /// The content description. - public string ContentDescription { + public string? ContentDescription { get; set; } @@ -83,7 +101,7 @@ public string ContentDescription { /// method to parse this value into a usable . /// /// The content transfer encoding. - public string ContentTransferEncoding { + public string? ContentTransferEncoding { get; set; } @@ -91,7 +109,9 @@ public string ContentTransferEncoding { /// Gets the size of the body part, in bytes. /// /// - /// Gets the size of the body part, in bytes. + /// Gets the size of the body part, in bytes. + /// Note that this size is the size in its transfer encoding + /// and not the resulting size after any decoding. /// /// The number of octets. public uint Octets { @@ -105,7 +125,7 @@ public uint Octets { /// Gets the MD5 hash of the content, if available. /// /// The content md5. - public string ContentMd5 { + public string? ContentMd5 { get; set; } @@ -119,7 +139,7 @@ public string ContentMd5 { /// summary information from an . /// /// The content disposition. - public ContentDisposition ContentDisposition { + public ContentDisposition? ContentDisposition { get; set; } @@ -133,7 +153,7 @@ public ContentDisposition ContentDisposition { /// summary information from an . /// /// The content language. - public string[] ContentLanguage { + public string[]? ContentLanguage { get; set; } @@ -147,7 +167,7 @@ public string[] ContentLanguage { /// summary information from an . /// /// The content location. - public Uri ContentLocation { + public Uri? ContentLocation { get; set; } @@ -161,7 +181,7 @@ public Uri ContentLocation { /// is necessary to include the flag when /// fetching summary information from an . /// - /// true if this part is an attachment; otherwise, false. + /// if this part is an attachment; otherwise, . public bool IsAttachment { get { return ContentDisposition != null && ContentDisposition.IsAttachment; } } @@ -177,17 +197,16 @@ public bool IsAttachment { /// fetching summary information from an . /// /// The name of the file. - public string FileName { + public string? FileName { get { - string filename = null; + string? filename = null; if (ContentDisposition != null) filename = ContentDisposition.FileName; - if (filename == null) - filename = ContentType.Name; + filename ??= ContentType.Name; - return filename != null ? filename.Trim () : null; + return filename?.Trim (); } } @@ -204,7 +223,7 @@ public string FileName { /// /// The visitor. /// - /// is null. + /// is . /// public override void Accept (BodyPartVisitor visitor) { diff --git a/MailKit/BodyPartCollection.cs b/MailKit/BodyPartCollection.cs index d98852d663..2ac68f6e16 100644 --- a/MailKit/BodyPartCollection.cs +++ b/MailKit/BodyPartCollection.cs @@ -1,9 +1,9 @@ -// +// // BodyPartCollection.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -69,7 +69,7 @@ public int Count { /// /// Gets whether or not this body part collection is read only. /// - /// true if this collection is read only; otherwise, false. + /// if this collection is read only; otherwise, . public bool IsReadOnly { get { return false; } } @@ -82,7 +82,7 @@ public bool IsReadOnly { /// /// The body part. /// - /// is null. + /// is . /// public void Add (BodyPart part) { @@ -109,10 +109,10 @@ public void Clear () /// /// Determines whether or not the collection contains the specified body part. /// - /// true if the specified body part exists; otherwise false. + /// if the specified body part exists; otherwise, . /// The body part. /// - /// is null. + /// is . /// public bool Contains (BodyPart part) { @@ -132,7 +132,7 @@ public bool Contains (BodyPart part) /// The array. /// The array index. /// - /// is null. + /// is . /// /// /// is out of range. @@ -154,10 +154,10 @@ public void CopyTo (BodyPart[] array, int arrayIndex) /// /// Removes the specified body part. /// - /// true if the body part was removed; otherwise false. + /// if the body part was removed; otherwise, . /// The body part. /// - /// is null. + /// is . /// public bool Remove (BodyPart part) { @@ -202,38 +202,33 @@ public BodyPart this [int index] { /// The index of the part matching the specified URI if found; otherwise -1. /// The URI of the body part. /// - /// is null. + /// is . /// public int IndexOf (Uri uri) { if (uri == null) throw new ArgumentNullException (nameof (uri)); - bool cid = uri.IsAbsoluteUri && uri.Scheme.ToLowerInvariant () == "cid"; + bool cid = uri.IsAbsoluteUri && uri.Scheme.Equals ("cid", StringComparison.OrdinalIgnoreCase); for (int index = 0; index < Count; index++) { - var bodyPart = this[index] as BodyPartBasic; - - if (bodyPart == null) + if (this[index] is not BodyPartBasic bodyPart) continue; if (uri.IsAbsoluteUri) { if (cid) { if (!string.IsNullOrEmpty (bodyPart.ContentId)) { - var id = MimeUtils.EnumerateReferences (bodyPart.ContentId).FirstOrDefault (); + // Note: we might have a Content-Id in the form "", so attempt to decode it + var id = MimeUtils.EnumerateReferences (bodyPart.ContentId!).FirstOrDefault () ?? bodyPart.ContentId; if (id == uri.AbsolutePath) return index; } } else if (bodyPart.ContentLocation != null) { - Uri absolute; - if (!bodyPart.ContentLocation.IsAbsoluteUri) continue; - absolute = bodyPart.ContentLocation; - - if (absolute == uri) + if (bodyPart.ContentLocation == uri) return index; } } else if (bodyPart.ContentLocation == uri) { diff --git a/MailKit/BodyPartMessage.cs b/MailKit/BodyPartMessage.cs index 094eecaaa2..3517812b6a 100644 --- a/MailKit/BodyPartMessage.cs +++ b/MailKit/BodyPartMessage.cs @@ -1,9 +1,9 @@ -// +// // BodyPartMessage.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -27,6 +27,8 @@ using System; using System.Text; +using MimeKit; + namespace MailKit { /// /// A message/rfc822 body part. @@ -42,7 +44,25 @@ public class BodyPartMessage : BodyPartBasic /// /// Creates a new . /// - public BodyPartMessage () + [Obsolete ("Use BodyPartMessage (ContentType, string) instead.")] + public BodyPartMessage () : this (new ContentType ("message", "rfc822"), string.Empty) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The content type. + /// The part specifier. + /// + /// is . + /// -or- + /// is . + /// + public BodyPartMessage (ContentType contentType, string partSpecifier) : base (contentType, partSpecifier) { } @@ -53,7 +73,7 @@ public BodyPartMessage () /// Gets the envelope of the message, if available. /// /// The envelope. - public Envelope Envelope { + public Envelope? Envelope { get; set; } @@ -64,7 +84,7 @@ public Envelope Envelope { /// Gets the body structure of the message. /// /// The body structure. - public BodyPart Body { + public BodyPart? Body { get; set; } @@ -92,7 +112,7 @@ public uint Lines { /// /// The visitor. /// - /// is null. + /// is . /// public override void Accept (BodyPartVisitor visitor) { diff --git a/MailKit/BodyPartMultipart.cs b/MailKit/BodyPartMultipart.cs index 8395b4e5b5..7e1a8df9ba 100644 --- a/MailKit/BodyPartMultipart.cs +++ b/MailKit/BodyPartMultipart.cs @@ -1,9 +1,9 @@ -// +// // BodyPartMultipart.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -44,11 +44,53 @@ public class BodyPartMultipart : BodyPart /// /// Creates a new . /// - public BodyPartMultipart () + [Obsolete ("Use BodyPartMultipart (ContentType, string) instead.")] + public BodyPartMultipart () : this (new ContentType ("multipart", "mixed"), string.Empty) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The content type. + /// The part specifier. + /// + /// is . + /// -or- + /// is . + /// + public BodyPartMultipart (ContentType contentType, string partSpecifier) : base (contentType, partSpecifier) { BodyParts = new BodyPartCollection (); } + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The content type. + /// The part specifier. + /// The child body parts of the multipart. + /// + /// is . + /// -or- + /// is . + /// -or- + /// is . + /// + public BodyPartMultipart (ContentType contentType, string partSpecifier, BodyPartCollection bodyParts) : base (contentType, partSpecifier) + { + if (bodyParts is null) + throw new ArgumentNullException (nameof (bodyParts)); + + BodyParts = bodyParts; + } + /// /// Gets the child body parts. /// @@ -67,7 +109,7 @@ public BodyPartCollection BodyParts { /// Gets the Content-Disposition of the body part, if available. /// /// The content disposition. - public ContentDisposition ContentDisposition { + public ContentDisposition? ContentDisposition { get; set; } @@ -78,7 +120,7 @@ public ContentDisposition ContentDisposition { /// Gets the Content-Language of the body part, if available. /// /// The content language. - public string[] ContentLanguage { + public string[]? ContentLanguage { get; set; } @@ -89,7 +131,7 @@ public string[] ContentLanguage { /// Gets the Content-Location of the body part, if available. /// /// The content location. - public Uri ContentLocation { + public Uri? ContentLocation { get; set; } @@ -106,7 +148,7 @@ public Uri ContentLocation { /// /// The visitor. /// - /// is null. + /// is . /// public override void Accept (BodyPartVisitor visitor) { diff --git a/MailKit/BodyPartText.cs b/MailKit/BodyPartText.cs index 5dc1e88d2e..1924b94be9 100644 --- a/MailKit/BodyPartText.cs +++ b/MailKit/BodyPartText.cs @@ -1,9 +1,9 @@ -// +// // BodyPartText.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -27,6 +27,8 @@ using System; using System.Text; +using MimeKit; + namespace MailKit { /// /// A textual body part. @@ -35,7 +37,7 @@ namespace MailKit { /// Represents any body part with a media type of "text". /// /// - /// + /// /// public class BodyPartText : BodyPartBasic { @@ -45,7 +47,25 @@ public class BodyPartText : BodyPartBasic /// /// Creates a new . /// - public BodyPartText () + [Obsolete ("Use BodyPartText (ContentType, string) instead.")] + public BodyPartText () : this (new ContentType ("text", "plain"), string.Empty) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The content type. + /// The part specifier. + /// + /// is . + /// -or- + /// is . + /// + public BodyPartText (ContentType contentType, string partSpecifier) : base (contentType, partSpecifier) { } @@ -55,7 +75,7 @@ public BodyPartText () /// /// Checks whether or not the text part's Content-Type is text/plain. /// - /// true if the text is html; otherwise, false. + /// if the text is html; otherwise, . public bool IsPlain { get { return ContentType.IsMimeType ("text", "plain"); } } @@ -66,7 +86,7 @@ public bool IsPlain { /// /// Checks whether or not the text part's Content-Type is text/html. /// - /// true if the text is html; otherwise, false. + /// if the text is html; otherwise, . public bool IsHtml { get { return ContentType.IsMimeType ("text", "html"); } } @@ -95,7 +115,7 @@ public uint Lines { /// /// The visitor. /// - /// is null. + /// is . /// public override void Accept (BodyPartVisitor visitor) { diff --git a/MailKit/BodyPartVisitor.cs b/MailKit/BodyPartVisitor.cs index 8d896c5d98..78160968b2 100644 --- a/MailKit/BodyPartVisitor.cs +++ b/MailKit/BodyPartVisitor.cs @@ -1,9 +1,9 @@ -// +// // BodyPartVisitor.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -34,6 +34,16 @@ namespace MailKit { /// public abstract class BodyPartVisitor { + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + protected BodyPartVisitor () + { + } + /// /// Dispatches the entity to one of the more specialized visit methods in this class. /// @@ -43,8 +53,7 @@ public abstract class BodyPartVisitor /// The MIME body part. public virtual void Visit (BodyPart body) { - if (body != null) - body.Accept (this); + body?.Accept (this); } /// @@ -79,8 +88,7 @@ protected internal virtual void VisitBodyPartBasic (BodyPartBasic entity) /// The body part representing the message/rfc822 message. protected virtual void VisitMessage (BodyPart message) { - if (message != null) - message.Accept (this); + message.Accept (this); } /// @@ -93,7 +101,9 @@ protected virtual void VisitMessage (BodyPart message) protected internal virtual void VisitBodyPartMessage (BodyPartMessage entity) { VisitBodyPartBasic (entity); - VisitMessage (entity.Body); + + if (entity.Body != null) + VisitMessage (entity.Body); } /// diff --git a/MailKit/ByteArrayBuilder.cs b/MailKit/ByteArrayBuilder.cs new file mode 100644 index 0000000000..a22b7a3994 --- /dev/null +++ b/MailKit/ByteArrayBuilder.cs @@ -0,0 +1,182 @@ +// +// ByteArrayBuilder.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Text; +using System.Buffers; + +namespace MailKit +{ + class ByteArrayBuilder : IDisposable + { + byte[] buffer; + int length; + + public ByteArrayBuilder (int initialCapacity) + { + buffer = ArrayPool.Shared.Rent (initialCapacity); + length = 0; + } + + public int Length { + get { return length; } + } + + void EnsureCapacity (int capacity) + { + if (capacity > buffer.Length) { + var resized = ArrayPool.Shared.Rent (capacity); + Buffer.BlockCopy (buffer, 0, resized, 0, length); + ArrayPool.Shared.Return (buffer); + buffer = resized; + } + } + + public void Append (byte c) + { + EnsureCapacity (length + 1); + buffer[length++] = c; + } + + public void Append (byte[] text, int startIndex, int count) + { + EnsureCapacity (length + count); + Buffer.BlockCopy (text, startIndex, buffer, length, count); + length += count; + } + + public void Clear () + { + length = 0; + } + + public byte[] ToArray () + { + var array = new byte[length]; + + Buffer.BlockCopy (buffer, 0, array, 0, length); + + return array; + } + + public string ToString (Encoding encoding, Encoding fallback) + { + try { + return encoding.GetString (buffer, 0, length); + } catch (DecoderFallbackException) { + return fallback.GetString (buffer, 0, length); + } + } + + public override string ToString () + { + return ToString (TextEncodings.UTF8, TextEncodings.Latin1); + } + + public bool Equals (string value, bool ignoreCase = false) + { + if (length == value.Length) { + if (ignoreCase) { + for (int i = 0; i < length; i++) { + uint a = (uint) buffer[i]; + uint b = (uint) value[i]; + + if ((a - 'a') <= 'z' - 'a') + a -= 0x20; + if ((b - 'a') <= 'z' - 'a') + b -= 0x20; + + if (a != b) + return false; + } + } else { + for (int i = 0; i < length; i++) { + if (value[i] != (char) buffer[i]) + return false; + } + } + + return true; + } + + return false; + } + + public void TrimNewLine () + { + // Trim the sequence from the end of the line. + if (length > 0 && buffer[length - 1] == (byte) '\n') { + length--; + + if (length > 0 && buffer[length - 1] == (byte) '\r') + length--; + } + } + + // FIXME: This should be moved somewhere else... + internal static bool TryParse (byte[] text, ref int index, int endIndex, out int value) + { + int startIndex = index; + + value = 0; + + while (index < endIndex && text[index] >= (byte) '0' && text[index] <= (byte) '9') { + int digit = text[index] - (byte) '0'; + + if (value > int.MaxValue / 10) { + // integer overflow + return false; + } + + if (value == int.MaxValue / 10 && digit > int.MaxValue % 10) { + // integer overflow + return false; + } + + value = (value * 10) + digit; + index++; + } + + return index > startIndex; + } + + // FIXME: Does this make sense to have here? Or should I have an extensions class for byte[] that has this? + public bool TryParse (int startIndex, int endIndex, out int value) + { + int index = startIndex; + + return TryParse (buffer, ref index, endIndex, out value); + } + + public void Dispose () + { + if (length != -1) { + ArrayPool.Shared.Return (buffer); + length = -1; + } + } + } +} diff --git a/MailKit/CommandException.cs b/MailKit/CommandException.cs index 8e3cfb4464..e3b830a642 100644 --- a/MailKit/CommandException.cs +++ b/MailKit/CommandException.cs @@ -1,9 +1,9 @@ -// +// // CommandException.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -55,9 +55,10 @@ public abstract class CommandException : Exception /// The serialization info. /// The streaming context. /// - /// is null. + /// is . /// [SecuritySafeCritical] + [Obsolete ("This API supports obsolete formatter-based serialization. It should not be called or extended by application code.")] protected CommandException (SerializationInfo info, StreamingContext context) : base (info, context) { } diff --git a/MailKit/CompressedStream.cs b/MailKit/CompressedStream.cs index 180eae7199..a8cfc0e700 100644 --- a/MailKit/CompressedStream.cs +++ b/MailKit/CompressedStream.cs @@ -1,9 +1,9 @@ -// +// // CompressedStream.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -26,6 +26,8 @@ using System; using System.IO; +using System.Threading; +using System.Threading.Tasks; using Org.BouncyCastle.Utilities.Zlib; @@ -38,9 +40,9 @@ class CompressedStream : Stream readonly ZStream zIn, zOut; bool eos, disposed; - public CompressedStream (Stream baseStream) + public CompressedStream (Stream innerStream) { - BaseStream = baseStream; + InnerStream = innerStream; zOut = new ZStream (); zOut.deflateInit (5, true); @@ -52,33 +54,33 @@ public CompressedStream (Stream baseStream) } /// - /// Gets the base stream. + /// Gets the inner stream. /// - /// The base stream. - public Stream BaseStream { + /// The inner stream. + public Stream InnerStream { get; private set; } /// /// Gets whether the stream supports reading. /// - /// true if the stream supports reading; otherwise, false. + /// if the stream supports reading; otherwise, . public override bool CanRead { - get { return BaseStream.CanRead; } + get { return InnerStream.CanRead; } } /// /// Gets whether the stream supports writing. /// - /// true if the stream supports writing; otherwise, false. + /// if the stream supports writing; otherwise, . public override bool CanWrite { - get { return BaseStream.CanWrite; } + get { return InnerStream.CanWrite; } } /// /// Gets whether the stream supports seeking. /// - /// true if the stream supports seeking; otherwise, false. + /// if the stream supports seeking; otherwise, . public override bool CanSeek { get { return false; } } @@ -86,29 +88,29 @@ public override bool CanSeek { /// /// Gets whether the stream supports I/O timeouts. /// - /// true if the stream supports I/O timeouts; otherwise, false. + /// if the stream supports I/O timeouts; otherwise, . public override bool CanTimeout { - get { return BaseStream.CanTimeout; } + get { return InnerStream.CanTimeout; } } /// - /// Gets or sets a value, in miliseconds, that determines how long the stream will attempt to read before timing out. + /// Gets or sets a value, in milliseconds, that determines how long the stream will attempt to read before timing out. /// - /// A value, in miliseconds, that determines how long the stream will attempt to read before timing out. + /// A value, in milliseconds, that determines how long the stream will attempt to read before timing out. /// The read timeout. public override int ReadTimeout { - get { return BaseStream.ReadTimeout; } - set { BaseStream.ReadTimeout = value; } + get { return InnerStream.ReadTimeout; } + set { InnerStream.ReadTimeout = value; } } /// - /// Gets or sets a value, in miliseconds, that determines how long the stream will attempt to write before timing out. + /// Gets or sets a value, in milliseconds, that determines how long the stream will attempt to write before timing out. /// - /// A value, in miliseconds, that determines how long the stream will attempt to write before timing out. + /// A value, in milliseconds, that determines how long the stream will attempt to write before timing out. /// The write timeout. public override int WriteTimeout { - get { return BaseStream.WriteTimeout; } - set { BaseStream.WriteTimeout = value; } + get { return InnerStream.WriteTimeout; } + set { InnerStream.WriteTimeout = value; } } /// @@ -164,12 +166,12 @@ void CheckDisposed () /// The buffer offset. /// The number of bytes to read. /// - /// is null. + /// is . /// /// /// is less than zero or greater than the length of . /// -or- - /// The is not large enough to contain bytes strting + /// The is not large enough to contain bytes starting /// at the specified . /// /// @@ -193,7 +195,71 @@ public override int Read (byte[] buffer, int offset, int count) do { if (zIn.avail_in == 0 && !eos) { - zIn.avail_in = BaseStream.Read (zIn.next_in, 0, zIn.next_in.Length); + zIn.avail_in = InnerStream.Read (zIn.next_in, 0, zIn.next_in.Length); + + eos = zIn.avail_in == 0; + zIn.next_in_index = 0; + } + + int retval = zIn.inflate (JZlib.Z_FULL_FLUSH); + + if (retval == JZlib.Z_STREAM_END) + break; + + if (eos && retval == JZlib.Z_BUF_ERROR) + return 0; + + if (retval != JZlib.Z_OK) + throw new IOException ("Error inflating: " + zIn.msg); + } while (zIn.avail_out == count); + + return count - zIn.avail_out; + } + + /// + /// Reads a sequence of bytes from the stream and advances the position + /// within the stream by the number of bytes read. + /// + /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many + /// bytes are not currently available, or zero (0) if the end of the stream has been reached. + /// The buffer. + /// The buffer offset. + /// The number of bytes to read. + /// The cancellation token. + /// + /// is . + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes starting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// An I/O error occurred. + /// + public override async Task ReadAsync (byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + CheckDisposed (); + + ValidateArguments (buffer, offset, count); + + if (count == 0) + return 0; + + zIn.next_out = buffer; + zIn.next_out_index = offset; + zIn.avail_out = count; + + do { + if (zIn.avail_in == 0 && !eos) { + cancellationToken.ThrowIfCancellationRequested (); + + zIn.avail_in = await InnerStream.ReadAsync (zIn.next_in, 0, zIn.next_in.Length, cancellationToken).ConfigureAwait (false); + eos = zIn.avail_in == 0; zIn.next_in_index = 0; } @@ -217,16 +283,16 @@ public override int Read (byte[] buffer, int offset, int count) /// Writes a sequence of bytes to the stream and advances the current /// position within this stream by the number of bytes written. /// - /// The buffer to write. - /// The offset of the first byte to write. - /// The number of bytes to write. + /// The buffer to write. + /// The offset of the first byte to write. + /// The number of bytes to write. /// - /// is null. + /// is . /// /// /// is less than zero or greater than the length of . /// -or- - /// The is not large enough to contain bytes strting + /// The is not large enough to contain bytes starting /// at the specified . /// /// @@ -258,7 +324,60 @@ public override void Write (byte[] buffer, int offset, int count) if (zOut.deflate (JZlib.Z_FULL_FLUSH) != JZlib.Z_OK) throw new IOException ("Error deflating: " + zOut.msg); - BaseStream.Write (zOut.next_out, 0, zOut.next_out.Length - zOut.avail_out); + InnerStream.Write (zOut.next_out, 0, zOut.next_out.Length - zOut.avail_out); + } while (zOut.avail_in > 0 || zOut.avail_out == 0); + } + + /// + /// Writes a sequence of bytes to the stream and advances the current + /// position within this stream by the number of bytes written. + /// + /// A task that represents the asynchronous write operation. + /// The buffer to write. + /// The offset of the first byte to write. + /// The number of bytes to write. + /// The cancellation token. + /// + /// is . + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes starting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support writing. + /// + /// + /// An I/O error occurred. + /// + public override async Task WriteAsync (byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + CheckDisposed (); + + ValidateArguments (buffer, offset, count); + + if (count == 0) + return; + + zOut.next_in = buffer; + zOut.next_in_index = offset; + zOut.avail_in = count; + + do { + cancellationToken.ThrowIfCancellationRequested (); + + zOut.avail_out = zOut.next_out.Length; + zOut.next_out_index = 0; + + if (zOut.deflate (JZlib.Z_FULL_FLUSH) != JZlib.Z_OK) + throw new IOException ("Error deflating: " + zOut.msg); + + await InnerStream.WriteAsync (zOut.next_out, 0, zOut.next_out.Length - zOut.avail_out, cancellationToken).ConfigureAwait (false); } while (zOut.avail_in > 0 || zOut.avail_out == 0); } @@ -279,7 +398,28 @@ public override void Flush () { CheckDisposed (); - BaseStream.Flush (); + InnerStream.Flush (); + } + + /// + /// Clears all output buffers for this stream and causes any buffered data to be written + /// to the underlying device. + /// + /// A task that represents the asynchronous flush operation. + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support writing. + /// + /// + /// An I/O error occurred. + /// + public override Task FlushAsync (CancellationToken cancellationToken) + { + CheckDisposed (); + + return InnerStream.FlushAsync (cancellationToken); } /// @@ -309,15 +449,15 @@ public override void SetLength (long value) } /// - /// Releases the unmanaged resources used by the and + /// Releases the unmanaged resources used by the and /// optionally releases the managed resources. /// - /// true to release both managed and unmanaged resources; - /// false to release only the unmanaged resources. + /// to release both managed and unmanaged resources; + /// to release only the unmanaged resources. protected override void Dispose (bool disposing) { if (disposing && !disposed) { - BaseStream.Dispose (); + InnerStream.Dispose (); disposed = true; zOut.free (); zIn.free (); diff --git a/MailKit/ConnectedEventArgs.cs b/MailKit/ConnectedEventArgs.cs new file mode 100644 index 0000000000..12e898c3a8 --- /dev/null +++ b/MailKit/ConnectedEventArgs.cs @@ -0,0 +1,91 @@ +// +// ConnectedEventArgs.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +using MailKit.Security; + +namespace MailKit +{ + /// + /// Connected event arguments. + /// + /// + /// When a is connected, it will emit a + /// event. + /// + public class ConnectedEventArgs : EventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The name of the host that the client connected to. + /// The port that the client connected to on the remote host. + /// The SSL/TLS options that were used when connecting to the remote host. + public ConnectedEventArgs (string host, int port, SecureSocketOptions options) + { + Options = options; + Host = host; + Port = port; + } + + /// + /// Get the name of the remote host. + /// + /// + /// Gets the name of the remote host. + /// + /// The host name of the server. + public string Host { + get; private set; + } + + /// + /// Get the port. + /// + /// + /// Gets the port. + /// + /// The port. + public int Port { + get; private set; + } + + /// + /// Get the SSL/TLS options. + /// + /// + /// Gets the SSL/TLS options. + /// + /// The SSL/TLS options. + public SecureSocketOptions Options { + get; private set; + } + } +} diff --git a/MailKit/DeliveryStatusNotification.cs b/MailKit/DeliveryStatusNotification.cs index 747b9db9a3..20af530a8f 100644 --- a/MailKit/DeliveryStatusNotification.cs +++ b/MailKit/DeliveryStatusNotification.cs @@ -1,9 +1,9 @@ -// +// // DeliveryStatusNotification.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -33,8 +33,11 @@ namespace MailKit { /// /// A set of flags that may be bitwise-or'd together to specify /// when a delivery status notification should be sent for a - /// particlar recipient. + /// particular recipient. /// + /// + /// + /// [Flags] public enum DeliveryStatusNotification { /// diff --git a/MailKit/DeliveryStatusNotificationType.cs b/MailKit/DeliveryStatusNotificationType.cs new file mode 100644 index 0000000000..16b9456e04 --- /dev/null +++ b/MailKit/DeliveryStatusNotificationType.cs @@ -0,0 +1,58 @@ +// +// DeliveryStatusNotificationReturnType.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +// FIXME: Move this to the MailKit namespace. It wasn't ever supposed to be in MailKit.Net.Smtp! +namespace MailKit.Net.Smtp +{ + /// + /// Delivery status notification type. + /// + /// + /// The delivery status notification type specifies whether or not + /// the full message should be included in any failed DSN issued for + /// a message transmission as opposed to just the headers. + /// + /// + /// + /// + public enum DeliveryStatusNotificationType + { + /// + /// The return type is unspecified, allowing the server to choose. + /// + Unspecified, + + /// + /// The full message should be included in any failed delivery status notification issued by the server. + /// + Full, + + /// + /// Only the headers should be included in any failed delivery status notification issued by the server. + /// + HeadersOnly, + } +} diff --git a/MailKit/DisconnectedEventArgs.cs b/MailKit/DisconnectedEventArgs.cs new file mode 100644 index 0000000000..585f3d1cf9 --- /dev/null +++ b/MailKit/DisconnectedEventArgs.cs @@ -0,0 +1,70 @@ +// +// DisconnectedEventArgs.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using MailKit.Security; + +namespace MailKit +{ + /// + /// Disconnected event arguments. + /// + /// + /// When a gets disconnected, it will emit a + /// event. + /// + public class DisconnectedEventArgs : ConnectedEventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The name of the host that the client was connected to. + /// The port that the client was connected to. + /// The SSL/TLS options that were used by the client. + /// If , the was disconnected via the + /// method. + public DisconnectedEventArgs (string host, int port, SecureSocketOptions options, bool requested) : base (host, port, options) + { + IsRequested = requested; + } + + /// + /// Get whether or not the service was explicitly asked to disconnect. + /// + /// + /// If the was disconnected via the + /// method, then + /// the value of will be . If the connection was unexpectedly + /// dropped, then the value will be . + /// + /// if the disconnect was explicitly requested; otherwise, . + public bool IsRequested { + get; private set; + } + } +} diff --git a/MailKit/DuplexStream.cs b/MailKit/DuplexStream.cs index 2820c13bc8..cae4587e49 100644 --- a/MailKit/DuplexStream.cs +++ b/MailKit/DuplexStream.cs @@ -1,9 +1,9 @@ -// +// // DuplexStream.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -26,6 +26,8 @@ using System; using System.IO; +using System.Threading; +using System.Threading.Tasks; namespace MailKit { /// @@ -41,9 +43,9 @@ class DuplexStream : Stream /// The stream to use for input. /// The stream to use for output. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// public DuplexStream (Stream istream, Stream ostream) { @@ -76,7 +78,7 @@ public Stream OutputStream { /// /// Gets whether the stream supports reading. /// - /// true if the stream supports reading; otherwise, false. + /// if the stream supports reading; otherwise, . public override bool CanRead { get { return true; } } @@ -84,7 +86,7 @@ public override bool CanRead { /// /// Gets whether the stream supports writing. /// - /// true if the stream supports writing; otherwise, false. + /// if the stream supports writing; otherwise, . public override bool CanWrite { get { return true; } } @@ -92,7 +94,7 @@ public override bool CanWrite { /// /// Gets whether the stream supports seeking. /// - /// true if the stream supports seeking; otherwise, false. + /// if the stream supports seeking; otherwise, . public override bool CanSeek { get { return false; } } @@ -100,15 +102,15 @@ public override bool CanSeek { /// /// Gets whether the stream supports I/O timeouts. /// - /// true if the stream supports I/O timeouts; otherwise, false. + /// if the stream supports I/O timeouts; otherwise, . public override bool CanTimeout { get { return InputStream.CanTimeout && OutputStream.CanTimeout; } } /// - /// Gets or sets a value, in miliseconds, that determines how long the stream will attempt to read before timing out. + /// Gets or sets a value, in milliseconds, that determines how long the stream will attempt to read before timing out. /// - /// A value, in miliseconds, that determines how long the stream will attempt to read before timing out. + /// A value, in milliseconds, that determines how long the stream will attempt to read before timing out. /// The read timeout. public override int ReadTimeout { get { return InputStream.ReadTimeout; } @@ -116,9 +118,9 @@ public override int ReadTimeout { } /// - /// Gets or sets a value, in miliseconds, that determines how long the stream will attempt to write before timing out. + /// Gets or sets a value, in milliseconds, that determines how long the stream will attempt to write before timing out. /// - /// A value, in miliseconds, that determines how long the stream will attempt to write before timing out. + /// A value, in milliseconds, that determines how long the stream will attempt to write before timing out. /// The write timeout. public override int WriteTimeout { get { return OutputStream.WriteTimeout; } @@ -178,12 +180,12 @@ void CheckDisposed () /// The buffer offset. /// The number of bytes to read. /// - /// is null. + /// is . /// /// /// is less than zero or greater than the length of . /// -or- - /// The is not large enough to contain bytes strting + /// The is not large enough to contain bytes starting /// at the specified . /// /// @@ -201,20 +203,54 @@ public override int Read (byte[] buffer, int offset, int count) return InputStream.Read (buffer, offset, count); } + /// + /// Reads a sequence of bytes from the stream and advances the position + /// within the stream by the number of bytes read. + /// + /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many + /// bytes are not currently available, or zero (0) if the end of the stream has been reached. + /// The buffer. + /// The buffer offset. + /// The number of bytes to read. + /// The cancellation token. + /// + /// is . + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes starting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// An I/O error occurred. + /// + public override Task ReadAsync (byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + CheckDisposed (); + + ValidateArguments (buffer, offset, count); + + return InputStream.ReadAsync (buffer, offset, count, cancellationToken); + } + /// /// Writes a sequence of bytes to the stream and advances the current /// position within this stream by the number of bytes written. /// - /// The buffer to write. - /// The offset of the first byte to write. - /// The number of bytes to write. + /// The buffer to write. + /// The offset of the first byte to write. + /// The number of bytes to write. /// - /// is null. + /// is . /// /// /// is less than zero or greater than the length of . /// -or- - /// The is not large enough to contain bytes strting + /// The is not large enough to contain bytes starting /// at the specified . /// /// @@ -235,6 +271,42 @@ public override void Write (byte[] buffer, int offset, int count) OutputStream.Write (buffer, offset, count); } + /// + /// Writes a sequence of bytes to the stream and advances the current + /// position within this stream by the number of bytes written. + /// + /// A task that represents the asynchronous write operation. + /// The buffer to write. + /// The offset of the first byte to write. + /// The number of bytes to write. + /// The cancellation token. + /// + /// is . + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes starting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support writing. + /// + /// + /// An I/O error occurred. + /// + public override Task WriteAsync (byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + CheckDisposed (); + + ValidateArguments (buffer, offset, count); + + return OutputStream.WriteAsync (buffer, offset, count, cancellationToken); + } + /// /// Clears all output buffers for this stream and causes any buffered data to be written /// to the underlying device. @@ -255,6 +327,28 @@ public override void Flush () OutputStream.Flush (); } + /// + /// Clears all output buffers for this stream and causes any buffered data to be written + /// to the underlying device. + /// + /// A task that represents the asynchronous flush operation. + /// The cancellation token. + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support writing. + /// + /// + /// An I/O error occurred. + /// + public override Task FlushAsync (CancellationToken cancellationToken) + { + CheckDisposed (); + + return OutputStream.FlushAsync (cancellationToken); + } + /// /// Sets the position within the current stream. /// @@ -285,8 +379,8 @@ public override void SetLength (long value) /// Releases the unmanaged resources used by the and /// optionally releases the managed resources. /// - /// true to release both managed and unmanaged resources; - /// false to release only the unmanaged resources. + /// to release both managed and unmanaged resources; + /// to release only the unmanaged resources. protected override void Dispose (bool disposing) { if (disposing && !disposed) { diff --git a/MailKit/Envelope.cs b/MailKit/Envelope.cs index 32c86d5e6a..83c51535bf 100644 --- a/MailKit/Envelope.cs +++ b/MailKit/Envelope.cs @@ -1,9 +1,9 @@ -// +// // Envelope.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -27,6 +27,8 @@ using System; using System.Text; using System.Linq; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using MimeKit; using MimeKit.Utils; @@ -135,7 +137,7 @@ public InternetAddressList Bcc { /// The Message-Id that the message is replying to. /// /// The Message-Id that the message is replying to. - public string InReplyTo { + public string? InReplyTo { get; set; } @@ -157,7 +159,7 @@ public DateTimeOffset? Date { /// Gets the ID of the message, if available. /// /// The message identifier. - public string MessageId { + public string? MessageId { get; set; } @@ -168,7 +170,7 @@ public string MessageId { /// Gets the subject of the message. /// /// The subject. - public string Subject { + public string? Subject { get; set; } @@ -176,46 +178,60 @@ static void EncodeMailbox (StringBuilder builder, MailboxAddress mailbox) { builder.Append ('('); - if (mailbox.Name != null) - builder.AppendFormat ("{0} ", MimeUtils.Quote (mailbox.Name)); - else + if (mailbox.Name != null) { + MimeUtils.AppendQuoted (builder, mailbox.Name); + builder.Append (' '); + } else { builder.Append ("NIL "); + } - if (mailbox.Route.Count != 0) - builder.AppendFormat ("\"{0}\" ", mailbox.Route); - else + if (mailbox.Route.Count != 0) { + MimeUtils.AppendQuoted (builder, mailbox.Route.ToString ()); + builder.Append (' '); + } else { builder.Append ("NIL "); + } - if (mailbox.Address != null) { - int at = mailbox.Address.LastIndexOf ('@'); + int at = mailbox.Address.LastIndexOf ('@'); - if (at >= 0) { - var domain = mailbox.Address.Substring (at + 1); - var user = mailbox.Address.Substring (0, at); + if (at >= 0) { + var domain = mailbox.Address.Substring (at + 1); + var user = mailbox.Address.Substring (0, at); - builder.AppendFormat ("{0} {1}", MimeUtils.Quote (user), MimeUtils.Quote (domain)); - } else { - builder.AppendFormat ("{0} NIL", MimeUtils.Quote (mailbox.Address)); - } + MimeUtils.AppendQuoted (builder, user); + builder.Append (' '); + MimeUtils.AppendQuoted (builder, domain); } else { - builder.Append ("NIL NIL"); + MimeUtils.AppendQuoted (builder, mailbox.Address); + builder.Append (" \"localhost\""); } builder.Append (')'); } - static void EncodeAddressList (StringBuilder builder, InternetAddressList list) + static void EncodeInternetAddressListAddresses (StringBuilder builder, InternetAddressList addresses) { - if (list.Count == 0) { - builder.Append ("NIL"); - return; + foreach (var addr in addresses) { + if (addr is MailboxAddress mailbox) + EncodeMailbox (builder, mailbox); + else if (addr is GroupAddress group) + EncodeGroup (builder, group); } + } - builder.Append ('('); - - foreach (var mailbox in list.Mailboxes) - EncodeMailbox (builder, mailbox); + static void EncodeGroup (StringBuilder builder, GroupAddress group) + { + builder.Append ("(NIL NIL "); + MimeUtils.AppendQuoted (builder, group.Name ?? string.Empty); + builder.Append (" NIL)"); + EncodeInternetAddressListAddresses (builder, group.Members); + builder.Append ("(NIL NIL NIL NIL)"); + } + static void EncodeAddressList (StringBuilder builder, InternetAddressList list) + { + builder.Append ('('); + EncodeInternetAddressListAddresses (builder, list); builder.Append (')'); } @@ -223,15 +239,20 @@ internal void Encode (StringBuilder builder) { builder.Append ('('); - if (Date.HasValue) - builder.AppendFormat ("\"{0}\" ", DateUtils.FormatDate (Date.Value)); - else + if (Date.HasValue) { + builder.Append ('"'); + builder.Append (DateUtils.FormatDate (Date.Value)); + builder.Append ("\" "); + } else { builder.Append ("NIL "); + } - if (Subject != null) - builder.AppendFormat ("{0} ", MimeUtils.Quote (Subject)); - else + if (Subject != null) { + MimeUtils.AppendQuoted (builder, Subject); + builder.Append (' '); + } else { builder.Append ("NIL "); + } if (From.Count > 0) { EncodeAddressList (builder, From); @@ -276,20 +297,31 @@ internal void Encode (StringBuilder builder) } if (InReplyTo != null) { + string inReplyTo; + if (InReplyTo.Length > 1 && InReplyTo[0] != '<' && InReplyTo[InReplyTo.Length - 1] != '>') - builder.AppendFormat ("{0} ", MimeUtils.Quote ('<' + InReplyTo + '>')); + inReplyTo = '<' + InReplyTo + '>'; else - builder.AppendFormat ("{0} ", MimeUtils.Quote (InReplyTo)); - } else + inReplyTo = InReplyTo; + + MimeUtils.AppendQuoted (builder, inReplyTo); + builder.Append (' '); + } else { builder.Append ("NIL "); + } if (MessageId != null) { + string messageId; + if (MessageId.Length > 1 && MessageId[0] != '<' && MessageId[MessageId.Length - 1] != '>') - builder.AppendFormat ("{0}", MimeUtils.Quote ('<' + MessageId + '>')); + messageId = '<' + MessageId + '>'; else - builder.AppendFormat ("{0}", MimeUtils.Quote (MessageId)); - } else + messageId = MessageId; + + MimeUtils.AppendQuoted (builder, messageId); + } else { builder.Append ("NIL"); + } builder.Append (')'); } @@ -312,7 +344,12 @@ public override string ToString () return builder.ToString (); } - static bool TryParse (string text, ref int index, out string nstring) + static bool IsNIL (string text, int index) + { + return string.Compare (text, index, "NIL", 0, 3, StringComparison.Ordinal) == 0; + } + + static bool TryParse (string text, ref int index, out string? nstring) { nstring = null; @@ -323,7 +360,7 @@ static bool TryParse (string text, ref int index, out string nstring) return false; if (text[index] != '"') { - if (index + 3 <= text.Length && text.Substring (index, 3) == "NIL") { + if (index + 3 <= text.Length && IsNIL (text, index)) { index += 3; return true; } @@ -360,28 +397,25 @@ static bool TryParse (string text, ref int index, out string nstring) return true; } - static bool TryParse (string text, ref int index, out MailboxAddress mailbox) + static bool TryParse (string text, ref int index, out InternetAddress? addr) { - string name, route, user, domain, address; - DomainList domains; - - mailbox = null; + addr = null; if (text[index] != '(') return false; index++; - if (!TryParse (text, ref index, out name)) + if (!TryParse (text, ref index, out string? name)) return false; - if (!TryParse (text, ref index, out route)) + if (!TryParse (text, ref index, out string? route)) return false; - if (!TryParse (text, ref index, out user)) + if (!TryParse (text, ref index, out string? user)) return false; - if (!TryParse (text, ref index, out domain)) + if (!TryParse (text, ref index, out string? domain)) return false; while (index < text.Length && text[index] == ' ') @@ -392,20 +426,25 @@ static bool TryParse (string text, ref int index, out MailboxAddress mailbox) index++; - address = domain != null ? user + "@" + domain : user; + if (domain != null) { + user ??= "NIL"; - if (route != null && DomainList.TryParse (route, out domains)) - mailbox = new MailboxAddress (name, domains, address); - else - mailbox = new MailboxAddress (name, address); + // Note: The serializer injects "localhost" as the domain when provided a UNIX mailbox or the special <> mailbox. + var address = domain == "localhost" ? user : user + "@" + domain; + + if (route != null && DomainList.TryParse (route, out var domains)) + addr = new MailboxAddress (name, domains, address); + else + addr = new MailboxAddress (name, address); + } else if (user != null) { + addr = new GroupAddress (user); + } return true; } - static bool TryParse (string text, ref int index, out InternetAddressList list) + static bool TryParse (string text, ref int index, [NotNullWhen (true)] out InternetAddressList? list) { - MailboxAddress mailbox; - list = null; while (index < text.Length && text[index] == ' ') @@ -415,7 +454,7 @@ static bool TryParse (string text, ref int index, out InternetAddressList list) return false; if (text[index] != '(') { - if (index + 3 <= text.Length && text.Substring (index, 3) == "NIL") { + if (index + 3 <= text.Length && IsNIL (text, index)) { list = new InternetAddressList (); index += 3; return true; @@ -430,20 +469,37 @@ static bool TryParse (string text, ref int index, out InternetAddressList list) return false; list = new InternetAddressList (); + var stack = new List (); + int sp = 0; + + stack.Add (list); do { if (text[index] == ')') break; - if (!TryParse (text, ref index, out mailbox)) + if (!TryParse (text, ref index, out InternetAddress? addr)) return false; - list.Add (mailbox); + if (addr != null) { + stack[sp].Add (addr); + + if (addr is GroupAddress group) { + stack.Add (group.Members); + sp++; + } + } else if (sp > 0) { + stack.RemoveAt (sp); + sp--; + } while (index < text.Length && text[index] == ' ') index++; } while (index < text.Length); + // Note: technically, we should check that sp == 0 as well, since all groups should + // be popped off the stack, but in the interest of being liberal in what we accept, + // we'll ignore that. if (index >= text.Length) return false; @@ -452,10 +508,8 @@ static bool TryParse (string text, ref int index, out InternetAddressList list) return true; } - internal static bool TryParse (string text, ref int index, out Envelope envelope) + internal static bool TryParse (string text, ref int index, out Envelope? envelope) { - InternetAddressList from, sender, replyto, to, cc, bcc; - string inreplyto, messageid, subject, nstring; DateTimeOffset? date = null; envelope = null; @@ -464,7 +518,7 @@ internal static bool TryParse (string text, ref int index, out Envelope envelope index++; if (index >= text.Length || text[index] != '(') { - if (index + 3 <= text.Length && text.Substring (index, 3) == "NIL") { + if (index + 3 <= text.Length && IsNIL (text, index)) { index += 3; return true; } @@ -474,43 +528,41 @@ internal static bool TryParse (string text, ref int index, out Envelope envelope index++; - if (!TryParse (text, ref index, out nstring)) + if (!TryParse (text, ref index, out string? nstring)) return false; if (nstring != null) { - DateTimeOffset value; - - if (!DateUtils.TryParse (nstring, out value)) + if (!DateUtils.TryParse (nstring, out DateTimeOffset value)) return false; date = value; } - if (!TryParse (text, ref index, out subject)) + if (!TryParse (text, ref index, out string? subject)) return false; - if (!TryParse (text, ref index, out from)) + if (!TryParse (text, ref index, out InternetAddressList? from)) return false; - if (!TryParse (text, ref index, out sender)) + if (!TryParse (text, ref index, out InternetAddressList? sender)) return false; - if (!TryParse (text, ref index, out replyto)) + if (!TryParse (text, ref index, out InternetAddressList? replyto)) return false; - if (!TryParse (text, ref index, out to)) + if (!TryParse (text, ref index, out InternetAddressList? to)) return false; - if (!TryParse (text, ref index, out cc)) + if (!TryParse (text, ref index, out InternetAddressList? cc)) return false; - if (!TryParse (text, ref index, out bcc)) + if (!TryParse (text, ref index, out InternetAddressList? bcc)) return false; - if (!TryParse (text, ref index, out inreplyto)) + if (!TryParse (text, ref index, out string? inreplyto)) return false; - if (!TryParse (text, ref index, out messageid)) + if (!TryParse (text, ref index, out string? messageid)) return false; if (index >= text.Length || text[index] != ')') @@ -542,13 +594,13 @@ internal static bool TryParse (string text, ref int index, out Envelope envelope /// This syntax, while similar to IMAP's ENVELOPE syntax, is not /// completely compatible. /// - /// true, if the envelope was successfully parsed, false otherwise. + /// , if the envelope was successfully parsed, otherwise. /// The text to parse. /// The parsed envelope. /// - /// is null. + /// is . /// - public static bool TryParse (string text, out Envelope envelope) + public static bool TryParse (string text, out Envelope? envelope) { if (text == null) throw new ArgumentNullException (nameof (text)); @@ -559,4 +611,3 @@ public static bool TryParse (string text, out Envelope envelope) } } } - \ No newline at end of file diff --git a/MailKit/FetchRequest.cs b/MailKit/FetchRequest.cs new file mode 100644 index 0000000000..087304ec37 --- /dev/null +++ b/MailKit/FetchRequest.cs @@ -0,0 +1,151 @@ +// +// FetchRequest.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Collections.Generic; + +using MimeKit; + +namespace MailKit { + /// + /// A request for fetching various properties of a message. + /// + /// + /// A request for fetching various properties of a message. + /// + public class FetchRequest : IFetchRequest + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + public FetchRequest () + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The items to fetch. + public FetchRequest (MessageSummaryItems items) + { + Items = items; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The items to fetch. + /// The specific set of headers to fetch. + /// + /// is . + /// + /// + /// One or more of the specified is invalid. + /// + public FetchRequest (MessageSummaryItems items, IEnumerable headers) : this (items) + { + Headers = (headers is HeaderSet set) ? set : new HeaderSet (headers); + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The items to fetch. + /// The specific set of headers to fetch. + /// + /// is . + /// + /// + /// One or more of the specified is invalid. + /// + public FetchRequest (MessageSummaryItems items, IEnumerable headers) : this (items) + { + Headers = (headers is HeaderSet set) ? set : new HeaderSet (headers); + } + + /// + /// Get or set the mod-sequence value that indicates the last known state of the messages being requested. + /// + /// + /// Gets or sets the mod-sequence value that indicates the last known state of the messages being requested. + /// If this property is set, the results returned by Fetch + /// or FetchAsync will only include the message summaries which + /// have a higher mod-sequence value than the one specified. + /// If the mail store supports quick resynchronization and the application has enabled this feature via + /// , then the Fetch or FetchAsync method + /// will emit events for messages that were expunged from the folder after + /// the change specified by the mod-sequence value. + /// It should be noted that if another client has modified any message in the folder, the mail service may choose + /// to return information that was not explicitly requested. It is therefore important to be prepared to handle both + /// additional fields on a for messages that were requested as well as summaries for + /// messages that were not requested at all. + /// + /// The mod-sequence value that indicates the last known state of the messages being requested. + public ulong? ChangedSince { get; set; } + + /// + /// Get or set the message summary items to fetch. + /// + /// + /// Gets or sets the message summary items to fetch. + /// + /// The message summary items. + public MessageSummaryItems Items { get; set; } + + /// + /// Get the set of headers that will be fetched. + /// + /// + /// Gets the set of headers that will be fetched. + /// + /// The set of headers to be fetched. + public HeaderSet? Headers { get; set; } + +#if ENABLE_LAZY_PREVIEW_API + /// + /// Get or set options to use when fetching . + /// + /// + /// Gets or sets options to use when fetching . + /// These options are only used if includes the + /// value. + /// + public PreviewOptions PreviewOptions { get; set; } +#endif + } +} diff --git a/MailKit/FolderAccess.cs b/MailKit/FolderAccess.cs index a9a90cd640..2ca17a10ed 100644 --- a/MailKit/FolderAccess.cs +++ b/MailKit/FolderAccess.cs @@ -1,9 +1,9 @@ -// +// // FolderMode.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -32,7 +32,7 @@ namespace MailKit { /// A folder access mode. /// /// - /// + /// /// public enum FolderAccess { /// diff --git a/MailKit/FolderAttributes.cs b/MailKit/FolderAttributes.cs index ad214fed0d..810d8302c4 100644 --- a/MailKit/FolderAttributes.cs +++ b/MailKit/FolderAttributes.cs @@ -1,9 +1,9 @@ -// +// // FolderAttributes.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -107,24 +107,29 @@ public enum FolderAttributes { /// Flagged = (1 << 12), + /// + /// The folder is the special "Important" folder. + /// + Important = (1 << 13), + /// /// The folder is the special "Inbox" folder. /// - Inbox = (1 << 13), + Inbox = (1 << 14), /// /// The folder is the special "Junk" folder. /// - Junk = (1 << 14), + Junk = (1 << 15), /// /// The folder is the special "Sent" folder. /// - Sent = (1 << 15), + Sent = (1 << 16), /// /// The folder is the special "Trash" folder. /// - Trash = (1 << 16), + Trash = (1 << 17), } } diff --git a/MailKit/FolderCreatedEventArgs.cs b/MailKit/FolderCreatedEventArgs.cs new file mode 100644 index 0000000000..f5a085580f --- /dev/null +++ b/MailKit/FolderCreatedEventArgs.cs @@ -0,0 +1,67 @@ +// +// FolderCreatedEventArgs.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MailKit { + /// + /// Event args used when a is created. + /// + /// + /// Event args used when a is created. + /// + public class FolderCreatedEventArgs : EventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The newly created folder. + /// + /// is . + /// + public FolderCreatedEventArgs (IMailFolder folder) + { + if (folder == null) + throw new ArgumentNullException (nameof (folder)); + + Folder = folder; + } + + /// + /// Get the folder that was just created. + /// + /// + /// Gets the folder that was just created. + /// + /// The folder. + public IMailFolder Folder { + get; private set; + } + } +} diff --git a/MailKit/FolderFeature.cs b/MailKit/FolderFeature.cs new file mode 100644 index 0000000000..8015d9f8e1 --- /dev/null +++ b/MailKit/FolderFeature.cs @@ -0,0 +1,82 @@ +// +// FolderFeature.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +namespace MailKit +{ + /// + /// An optional feature that an may support. + /// + /// + /// An optional feature that an may support. + /// + public enum FolderFeature + { + /// + /// Indicates that the folder supports access rights. + /// + AccessRights, + + /// + /// Indicates that the folder allows arbitrary annotations to be set on a message. + /// + Annotations, + + /// + /// Indicates that the folder allows arbitrary metadata to be set. + /// + Metadata, + + /// + /// Indicates that the folder uses modification sequences for every state change of a message. + /// + ModSequences, + + /// + /// Indicates that the folder supports quick resynchronization when opening. + /// + QuickResync, + + /// + /// Indicates that the folder supports quotas. + /// + Quotas, + + /// + /// Indicates that the folder supports sorting messages. + /// + Sorting, + + /// + /// Indicates that the folder supports threading messages. + /// + Threading, + + /// + /// Indicates that the folder supports the use of UTF-8. + /// + UTF8, + } +} diff --git a/MailKit/FolderNamespace.cs b/MailKit/FolderNamespace.cs index 6a247787e5..5595b4451f 100644 --- a/MailKit/FolderNamespace.cs +++ b/MailKit/FolderNamespace.cs @@ -1,9 +1,9 @@ -// +// // FolderNamespace.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -60,7 +60,7 @@ public class FolderNamespace /// The directory separator. /// The folder path. /// - /// is null. + /// is . /// public FolderNamespace (char directorySeparator, string path) { diff --git a/MailKit/FolderNamespaceCollection.cs b/MailKit/FolderNamespaceCollection.cs index 374245fc15..ed6c168e94 100644 --- a/MailKit/FolderNamespaceCollection.cs +++ b/MailKit/FolderNamespaceCollection.cs @@ -1,9 +1,9 @@ -// +// // FolderNamespaceCollection.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -74,7 +74,7 @@ public int Count { /// /// The namespace to add. /// - /// is null. + /// is . /// public void Add (FolderNamespace @namespace) { @@ -101,11 +101,11 @@ public void Clear () /// /// Checks if the collection contains the specified namespace. /// - /// true if the specified namespace exists; - /// otherwise false. + /// if the specified namespace exists; + /// otherwise, . /// The namespace. /// - /// is null. + /// is . /// public bool Contains (FolderNamespace @namespace) { @@ -116,16 +116,16 @@ public bool Contains (FolderNamespace @namespace) } /// - /// Removes the first occurance of the specified namespace. + /// Removes the first occurrence of the specified namespace. /// /// - /// Removes the first occurance of the specified namespace. + /// Removes the first occurrence of the specified namespace. /// - /// true if the frst occurance of the specified - /// namespace was removed; otherwise false. + /// if the first occurrence of the specified + /// namespace was removed; otherwise, . /// The namespace. /// - /// is null. + /// is . /// public bool Remove (FolderNamespace @namespace) { @@ -144,7 +144,7 @@ public bool Remove (FolderNamespace @namespace) /// The folder namespace at the specified index. /// The index. /// - /// is null. + /// is . /// /// /// is out of range. @@ -224,8 +224,8 @@ public override string ToString () builder.Append ('\\'); builder.Append (namespaces[i].DirectorySeparator); builder.Append ("\" "); - builder.Append (MimeUtils.Quote (namespaces[i].Path)); - builder.Append (")"); + MimeUtils.AppendQuoted (builder, namespaces[i].Path); + builder.Append (')'); } builder.Append (')'); diff --git a/MailKit/FolderNotFoundException.cs b/MailKit/FolderNotFoundException.cs index ecb429e55b..55aa8b1380 100644 --- a/MailKit/FolderNotFoundException.cs +++ b/MailKit/FolderNotFoundException.cs @@ -1,9 +1,9 @@ -// +// // FolderNotFoundException.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -52,8 +52,9 @@ public class FolderNotFoundException : Exception /// The serialization info. /// The streaming context. /// - /// is null. + /// is . /// + [Obsolete ("This API supports obsolete formatter-based serialization. It should not be called or extended by application code.")] protected FolderNotFoundException (SerializationInfo info, StreamingContext context) : base (info, context) { FolderName = info.GetString ("FolderName"); @@ -70,7 +71,7 @@ protected FolderNotFoundException (SerializationInfo info, StreamingContext cont /// The name of the folder. /// The inner exception. /// - /// is null. + /// is . /// public FolderNotFoundException (string message, string folderName, Exception innerException) : base (message, innerException) { @@ -89,7 +90,7 @@ public FolderNotFoundException (string message, string folderName, Exception inn /// The error message. /// The name of the folder. /// - /// is null. + /// is . /// public FolderNotFoundException (string message, string folderName) : base (message) { @@ -107,7 +108,7 @@ public FolderNotFoundException (string message, string folderName) : base (messa /// /// The name of the folder. /// - /// is null. + /// is . /// public FolderNotFoundException (string folderName) : this ("The requested folder could not be found.", folderName) { @@ -135,17 +136,17 @@ public string FolderName { /// The serialization info. /// The streaming context. /// - /// is null. + /// is . /// [SecurityCritical] +#if NET8_0_OR_GREATER + [Obsolete ("This API supports obsolete formatter-based serialization. It should not be called or extended by application code.")] +#endif public override void GetObjectData (SerializationInfo info, StreamingContext context) { - if (info == null) - throw new ArgumentNullException (nameof (info)); + base.GetObjectData (info, context); info.AddValue ("FolderName", FolderName); - - base.GetObjectData (info, context); } #endif } diff --git a/MailKit/FolderNotOpenException.cs b/MailKit/FolderNotOpenException.cs index 73d64d4508..034cdd0d8a 100644 --- a/MailKit/FolderNotOpenException.cs +++ b/MailKit/FolderNotOpenException.cs @@ -1,9 +1,9 @@ -// +// // FolderNotOpenException.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -55,14 +55,14 @@ public class FolderNotOpenException : InvalidOperationException /// The serialization info. /// The streaming context. /// - /// is null. + /// is . /// + [Obsolete ("This API supports obsolete formatter-based serialization. It should not be called or extended by application code.")] protected FolderNotOpenException (SerializationInfo info, StreamingContext context) : base (info, context) { var value = info.GetString ("FolderAccess"); - FolderAccess access; - if (!Enum.TryParse (value, out access)) + if (!Enum.TryParse (value, out FolderAccess access)) FolderAccess = FolderAccess.ReadOnly; else FolderAccess = access; @@ -82,7 +82,7 @@ protected FolderNotOpenException (SerializationInfo info, StreamingContext conte /// The error message. /// The inner exception. /// - /// is null. + /// is . /// public FolderNotOpenException (string folderName, FolderAccess access, string message, Exception innerException) : base (message, innerException) { @@ -103,7 +103,7 @@ public FolderNotOpenException (string folderName, FolderAccess access, string me /// The minimum folder access required by the operation. /// The error message. /// - /// is null. + /// is . /// public FolderNotOpenException (string folderName, FolderAccess access, string message) : base (message) { @@ -123,7 +123,7 @@ public FolderNotOpenException (string folderName, FolderAccess access, string me /// The folder name. /// The minimum folder access required by the operation. /// - /// is null. + /// is . /// public FolderNotOpenException (string folderName, FolderAccess access) : this (folderName, access, GetDefaultMessage (access)) { @@ -170,17 +170,17 @@ static string GetDefaultMessage (FolderAccess access) /// The serialization info. /// The streaming context. /// - /// is null. + /// is . /// +#if NET8_0_OR_GREATER + [Obsolete ("This API supports obsolete formatter-based serialization. It should not be called or extended by application code.")] +#endif public override void GetObjectData (SerializationInfo info, StreamingContext context) { - if (info == null) - throw new ArgumentNullException (nameof (info)); + base.GetObjectData (info, context); info.AddValue ("FolderAccess", FolderAccess.ToString ()); info.AddValue ("FolderName", FolderName); - - base.GetObjectData (info, context); } #endif } diff --git a/MailKit/FolderQuota.cs b/MailKit/FolderQuota.cs index 28141e2926..89883c1d11 100644 --- a/MailKit/FolderQuota.cs +++ b/MailKit/FolderQuota.cs @@ -1,9 +1,9 @@ -// +// // FolderQuota.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -24,8 +24,6 @@ // THE SOFTWARE. // -using System; - namespace MailKit { /// /// A folder quota. @@ -45,7 +43,7 @@ public class FolderQuota /// Creates a new with the specified root. /// /// The quota root. - public FolderQuota (IMailFolder quotaRoot) + public FolderQuota (IMailFolder? quotaRoot) { QuotaRoot = quotaRoot; } @@ -54,14 +52,14 @@ public FolderQuota (IMailFolder quotaRoot) /// Get the quota root. /// /// - /// Gets the quota root. If the quota root is null, then + /// Gets the quota root. If the quota root is , then /// it suggests that the folder does not have a quota. /// /// /// /// /// The quota root. - public IMailFolder QuotaRoot { + public IMailFolder? QuotaRoot { get; private set; } diff --git a/MailKit/FolderRenamedEventArgs.cs b/MailKit/FolderRenamedEventArgs.cs index a9fd221b47..1c2b26b08d 100644 --- a/MailKit/FolderRenamedEventArgs.cs +++ b/MailKit/FolderRenamedEventArgs.cs @@ -1,9 +1,9 @@ -// +// // FolderRenamedEventArgs.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -44,9 +44,9 @@ public class FolderRenamedEventArgs : EventArgs /// The old name of the folder. /// The new name of the folder. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// public FolderRenamedEventArgs (string oldName, string newName) { diff --git a/MailKit/HeaderSet.cs b/MailKit/HeaderSet.cs new file mode 100644 index 0000000000..624ccc7b2c --- /dev/null +++ b/MailKit/HeaderSet.cs @@ -0,0 +1,466 @@ +// +// HeaderSet.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Collections; +using System.Collections.Generic; + +using MimeKit; + +namespace MailKit { + /// + /// A set of headers. + /// + /// + /// A set of headers. + /// + public class HeaderSet : ICollection + { + const string AtomSafeCharacters = "!#$%&'*+-/=?^_`{|}~"; + + /// + /// A set of headers that only includes all headers. + /// + /// + /// When used with a , this pre-computed set of headers can be used + /// to fetch the entire list of headers for a message. + /// + public static readonly HeaderSet All = new HeaderSet () { Exclude = true, IsReadOnly = true }; + + /// + /// A set of headers that only includes the standard envelope headers. + /// + /// + /// When used with a , this pre-computed set of headers can be used + /// to fetch the standard envelope headers for a message. + /// + public static readonly HeaderSet Envelope = new HeaderSet (new HeaderId[] { + HeaderId.Sender, + HeaderId.From, + HeaderId.ReplyTo, + HeaderId.To, + HeaderId.Cc, + HeaderId.Bcc, + HeaderId.Subject, + HeaderId.Date, + HeaderId.MessageId, + HeaderId.InReplyTo + }) { IsReadOnly = true }; + + /// + /// A set of headers that only includes the References header. + /// + /// + /// When used with a , this pre-computed set of headers can be used + /// to fetch the References header for a message. Generally, this should be used in + /// combination with in order to have all of the + /// information needed to thread messages using the + /// threading algorithm. + /// + public static readonly HeaderSet References = new HeaderSet (new HeaderId[] { HeaderId.References }) { IsReadOnly = true }; + + readonly HashSet hash = new HashSet (StringComparer.Ordinal); + bool exclude; + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + public HeaderSet () + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The headers to include. + public HeaderSet (IEnumerable headers) + { + AddRange (headers); + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The headers to include. + public HeaderSet (IEnumerable headers) + { + AddRange (headers); + } + + void CheckReadOnly () + { + if (IsReadOnly) + throw new InvalidOperationException ("The HeaderSet is read-only."); + } + + /// + /// Get the number of headers in the set. + /// + /// + /// Gets the number of headers in the set. + /// + /// The number of headers. + public int Count { + get { return hash.Count; } + } + + /// + /// Get or set whether this set of headers is meant to be excluded when used with a . + /// + /// + /// Get or set whether this set of headers is meant to be excluded when used with a . + /// + /// if the headers are meant to be excluded; otherwise, . + /// + /// The operation is invalid because the is read-only. + /// + public bool Exclude { + get { return exclude; } + set { + CheckReadOnly (); + exclude = value; + } + } + + /// + /// Get whether or not the set of headers is read-only. + /// + /// + /// Gets whether or not the set of headers is read-only. + /// + /// if this instance is read only; otherwise, . + public bool IsReadOnly { + get; private set; + } + + static bool IsAsciiAtom (char c) + { + return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || AtomSafeCharacters.IndexOf (c) != -1; + } + + static bool IsValid (string header) + { + if (header.Length == 0) + return false; + + for (int i = 0; i < header.Length; i++) { + if (header[i] < 127 && !IsAsciiAtom (header[i])) + return false; + } + + return true; + } + + /// + /// Add the specified header. + /// + /// + /// Adds the specified header to the set of headers. + /// + /// if the header was added to the set; otherwise, . + /// The header to add. + /// + /// is not a valid . + /// + /// + /// The operation is invalid because the is read-only. + /// + public bool Add (HeaderId header) + { + if (header == HeaderId.Unknown) + throw new ArgumentOutOfRangeException (nameof (header)); + + CheckReadOnly (); + + return hash.Add (header.ToHeaderName ().ToUpperInvariant ()); + } + + /// + /// Add the specified header. + /// + /// + /// Adds the specified header to the set of headers. + /// + /// if the header was added to the set; otherwise, . + /// The header to add. + /// + /// is . + /// + /// + /// The operation is invalid because the is read-only. + /// + public bool Add (string header) + { + if (header == null) + throw new ArgumentNullException (nameof (header)); + + if (!IsValid (header)) + throw new ArgumentException ("The header field is invalid.", nameof (header)); + + CheckReadOnly (); + + return hash.Add (header.ToUpperInvariant ()); + } + + /// + /// Add the specified header. + /// + /// + /// Adds the specified header to the set of headers. + /// + /// The header to add. + /// + /// is . + /// + /// + /// The operation is invalid because the is read-only. + /// + void ICollection.Add (string item) + { + Add (item); + } + + /// + /// Add a collection of headers. + /// + /// + /// Adds the specified headers to the set of headers. + /// + /// The headers to add. + /// + /// is . + /// + /// + /// One or more of the specified is invalid. + /// + /// + /// The operation is invalid because the is read-only. + /// + public void AddRange (IEnumerable headers) + { + if (headers == null) + throw new ArgumentNullException (nameof (headers)); + + CheckReadOnly (); + + foreach (var header in headers) { + if (header == HeaderId.Unknown) + throw new ArgumentException ("One or more of the headers is invalid.", nameof (headers)); + + hash.Add (header.ToHeaderName ().ToUpperInvariant ()); + } + } + + /// + /// Add a collection of headers. + /// + /// + /// Adds the specified headers to the set of headers. + /// + /// The headers to add. + /// + /// is . + /// + /// + /// One or more of the specified is invalid. + /// + /// + /// The operation is invalid because the is read-only. + /// + public void AddRange (IEnumerable headers) + { + if (headers == null) + throw new ArgumentNullException (nameof (headers)); + + CheckReadOnly (); + + foreach (var header in headers) { + if (header == null || !IsValid (header)) + throw new ArgumentException ("One or more of the headers is invalid.", nameof (headers)); + + hash.Add (header.ToUpperInvariant ()); + } + } + + /// + /// Clear the set of headers. + /// + /// + /// Clears the set of headers. + /// + /// + /// The operation is invalid because the is read-only. + /// + public void Clear () + { + CheckReadOnly (); + hash.Clear (); + } + + /// + /// Copy all of the headers in the to the specified array. + /// + /// + /// Copies all of the headers within the into the array, + /// starting at the specified array index. + /// + /// The array to copy the headers to. + /// The index into the array. + /// + /// is . + /// + /// + /// is out of range. + /// + public void CopyTo (string[] array, int arrayIndex) + { + hash.CopyTo (array, arrayIndex); + } + + /// + /// Check if the set of headers contains the specified header. + /// + /// + /// Determines whether or not the set of headers contains the specified header. + /// + /// if the specified header exists; + /// otherwise, . + /// The header identifier. + /// + /// is not a valid . + /// + public bool Contains (HeaderId header) + { + if (header == HeaderId.Unknown) + throw new ArgumentOutOfRangeException (nameof (header)); + + return hash.Contains (header.ToHeaderName ().ToUpperInvariant ()); + } + + /// + /// Check if the set of headers contains the specified header. + /// + /// + /// Determines whether or not the set of headers contains the specified header. + /// + /// if the specified header exists; + /// otherwise, . + /// The name of the header. + /// + /// is . + /// + public bool Contains (string header) + { + if (header == null) + throw new ArgumentNullException (nameof (header)); + + return hash.Contains (header.ToUpperInvariant ()); + } + + /// + /// Remove the specified header. + /// + /// + /// Removes the specified header if it exists. + /// + /// if the specified header was removed; + /// otherwise, . + /// The header. + /// + /// is not a valid . + /// + /// + /// The operation is invalid because the is read-only. + /// + public bool Remove (HeaderId header) + { + if (header == HeaderId.Unknown) + throw new ArgumentOutOfRangeException (nameof (header)); + + CheckReadOnly (); + + return hash.Remove (header.ToHeaderName ().ToUpperInvariant ()); + } + + /// + /// Remove the specified header. + /// + /// + /// Removes the specified header if it exists. + /// + /// if the specified header was removed; + /// otherwise, . + /// The header. + /// + /// is . + /// + /// + /// The operation is invalid because the is read-only. + /// + public bool Remove (string header) + { + if (header == null) + throw new ArgumentNullException (nameof (header)); + + CheckReadOnly (); + + return hash.Remove (header.ToUpperInvariant ()); + } + + /// + /// Get an enumerator for the set of headers. + /// + /// + /// Gets an enumerator for the set of headers. + /// + /// The enumerator. + public IEnumerator GetEnumerator () + { + return hash.GetEnumerator (); + } + + /// + /// Get an enumerator for the set of headers. + /// + /// + /// Gets an enumerator for the set of headers. + /// + /// The enumerator. + IEnumerator IEnumerable.GetEnumerator () + { + return hash.GetEnumerator (); + } + } +} diff --git a/MailKit/IAppendRequest.cs b/MailKit/IAppendRequest.cs new file mode 100644 index 0000000000..7b83ab0c77 --- /dev/null +++ b/MailKit/IAppendRequest.cs @@ -0,0 +1,100 @@ +// +// IAppendRequest.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Collections.Generic; + +using MimeKit; + +namespace MailKit { + /// + /// A request for appending a message to a folder. + /// + /// + /// A request for appending a message to a folder. + /// + public interface IAppendRequest + { + /// + /// Get the message that should be appended to the folder. + /// + /// + /// Gets the message that should be appended to the folder. + /// + /// The message. + MimeMessage Message { get; } + + /// + /// Get or set the message flags that should be set on the message. + /// + /// + /// Gets or sets the message flags that should be set on the message. + /// + /// The message flags. + MessageFlags Flags { get; set; } + + /// + /// Get or set the keywords that should be set on the message. + /// + /// + /// Gets or sets the keywords that should be set on the message. + /// + /// The keywords. + ISet? Keywords { get; set; } + + /// + /// Get or set the timestamp that should be used by folder as the . + /// + /// + /// Gets or sets the timestamp that should be used by folder as the . + /// + /// The date and time to use for the INTERNALDATE or if it should be left up to the folder to decide. + DateTimeOffset? InternalDate { get; set; } + + /// + /// Get or set the list of annotations that should be set on the message. + /// + /// + /// Gets or sets the list of annotations that should be set on the message. + /// + /// This feature is not supported by all folders. + /// Use with the enum value + /// to determine if this feature is supported. + /// + /// + /// The list of annotations. + IList? Annotations { get; set; } + + /// + /// Get or set the transfer progress reporting mechanism. + /// + /// + /// Gets or sets the transfer progress reporting mechanism. + /// + /// The transfer progress mechanism. + ITransferProgress? TransferProgress { get; set; } + } +} diff --git a/MailKit/IAuthenticationSecretDetector.cs b/MailKit/IAuthenticationSecretDetector.cs new file mode 100644 index 0000000000..c4f66bb336 --- /dev/null +++ b/MailKit/IAuthenticationSecretDetector.cs @@ -0,0 +1,93 @@ +// +// IAuthenticationSecretDetector.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Collections.Generic; + +namespace MailKit { + /// + /// An authentication secret. + /// + /// + /// An authentication secret. + /// + public struct AuthenticationSecret + { + /// + /// Get the starting offset of the secret within a buffer. + /// + /// + /// Gets the starting offset of the secret within a buffer. + /// + /// The start offset of the secret. + public int StartIndex { get; private set; } + + /// + /// Get the length of the secret within a buffer. + /// + /// + /// Gets the length of the secret within a buffer. + /// + /// The length of the secret. + public int Length { get; private set; } + + /// + /// Create a new . + /// + /// + /// Creates a new . + /// + /// The start index of the secret. + /// The length of the secret. + public AuthenticationSecret (int startIndex, int length) + { + StartIndex = startIndex; + Length = length; + } + } + + /// + /// An interface for detecting authentication secrets. + /// + /// + /// An interface for detecting authentication secrets. + /// + public interface IAuthenticationSecretDetector + + { + /// + /// Detect a list of secrets within a buffer. + /// + /// + /// Detects a list of secrets within a buffer. + /// + /// The buffer. + /// The buffer offset. + /// The length of the buffer. + /// A list of secrets. + IList DetectSecrets (byte[] buffer, int offset, int count); + } +} diff --git a/MailKit/IFetchRequest.cs b/MailKit/IFetchRequest.cs new file mode 100644 index 0000000000..30e984d4f0 --- /dev/null +++ b/MailKit/IFetchRequest.cs @@ -0,0 +1,86 @@ +// +// IFetchRequest.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +namespace MailKit { + /// + /// A request for fetching various properties of a message. + /// + /// + /// A request for fetching various properties of a message. + /// + public interface IFetchRequest + { + /// + /// Get or set the mod-sequence value that indicates the last known state of the messages being requested. + /// + /// + /// Gets or sets the mod-sequence value that indicates the last known state of the messages being requested. + /// If this property is set, the results returned by Fetch + /// or FetchAsync will only include the message summaries which + /// have a higher mod-sequence value than the one specified. + /// If the mail store supports quick resynchronization and the application has enabled this feature via + /// , then the Fetch or FetchAsync method + /// will emit events for messages that were expunged from the folder after + /// the change specified by the mod-sequence value. + /// It should be noted that if another client has modified any message in the folder, the mail service may choose + /// to return information that was not explicitly requested. It is therefore important to be prepared to handle both + /// additional fields on a for messages that were requested as well as summaries for + /// messages that were not requested at all. + /// + /// The mod-sequence value that indicates the last known state of the messages being requested. + ulong? ChangedSince { get; set; } + + /// + /// Get or set the message summary items to fetch. + /// + /// + /// Gets or sets the message summary items to fetch. + /// + /// The message summary items. + MessageSummaryItems Items { get; set; } + + /// + /// Get the set of headers that will be fetched. + /// + /// + /// Gets the set of headers that will be fetched. + /// + /// The set of headers to be fetched. + HeaderSet? Headers { get; } + +#if ENABLE_LAZY_PREVIEW_API + /// + /// Get the options to use when fetching . + /// + /// + /// Gets the options to use when fetching . + /// These options are only used if includes the + /// value. + /// + PreviewOptions PreviewOptions { get; } +#endif + } +} diff --git a/MailKit/IMailFolder.cs b/MailKit/IMailFolder.cs index 02ebcb5ab4..ad82962194 100644 --- a/MailKit/IMailFolder.cs +++ b/MailKit/IMailFolder.cs @@ -1,9 +1,9 @@ -// +// // IMailFolder.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -33,6 +33,12 @@ using MimeKit; using MailKit.Search; +#if NET5_0_OR_GREATER +using IReadOnlySetOfStrings = System.Collections.Generic.IReadOnlySet; +#else +using IReadOnlySetOfStrings = System.Collections.Generic.ISet; +#endif + namespace MailKit { /// /// An interface for a mailbox folder as used by . @@ -58,7 +64,7 @@ public interface IMailFolder : IEnumerable /// Root-level folders do not have a parent folder. /// /// The parent folder. - IMailFolder ParentFolder { get; } + IMailFolder? ParentFolder { get; } /// /// Get the folder attributes. @@ -69,15 +75,58 @@ public interface IMailFolder : IEnumerable /// The folder attributes. FolderAttributes Attributes { get; } + /// + /// Get the annotation access level. + /// + /// + /// If annotations are supported, this property can be used to determine whether or not + /// the supports reading and writing annotations. + /// + /// The annotation access level. + AnnotationAccess AnnotationAccess { get; } + + /// + /// Get the supported annotation scopes. + /// + /// + /// If annotations are supported, this property can be used to determine which + /// annotation scopes are supported by the . + /// + /// The supported annotation scopes. + AnnotationScope AnnotationScopes { get; } + + /// + /// Get the maximum size of annotation values supported by the folder. + /// + /// + /// If annotations are supported, this property can be used to determine the + /// maximum size of annotation values supported by the . + /// + /// The maximum size of annotation values supported by the folder. + uint MaxAnnotationSize { get; } + /// /// Get the permanent flags. /// /// - /// The permanent flags are the message flags that will persist between sessions. + /// The permanent flags are the message flags that will persist between sessions. + /// If the flag is set, then the folder allows + /// storing of user-defined (custom) message flags. /// /// The permanent flags. MessageFlags PermanentFlags { get; } + /// + /// Get the permanent keywords. + /// + /// + /// The permanent keywords are the keywords that will persist between sessions. + /// If the flag is set in , + /// then the folder allows storing of user-defined keywords as well. + /// + /// The permanent keywords. + IReadOnlySetOfStrings PermanentKeywords { get; } + /// /// Get the accepted flags. /// @@ -89,6 +138,17 @@ public interface IMailFolder : IEnumerable /// The accepted flags. MessageFlags AcceptedFlags { get; } + /// + /// Get the accepted keywords. + /// + /// + /// The accepted keywords are the keywords that will be accepted and persist + /// for the current session. For the set of keywords that will persist between + /// sessions, see the property. + /// + /// The accepted keywords. + IReadOnlySetOfStrings AcceptedKeywords { get; } + /// /// Get the directory separator. /// @@ -113,7 +173,7 @@ public interface IMailFolder : IEnumerable /// /// Gets whether or not the folder is a namespace folder. /// - /// true if the folder is a namespace folder; otherwise, false. + /// if the folder is a namespace folder; otherwise, . bool IsNamespace { get; } /// @@ -134,13 +194,26 @@ public interface IMailFolder : IEnumerable /// The name of the folder. string Name { get; } + /// + /// Get the unique identifier for the folder, if available. + /// + /// + /// Gets a unique identifier for the folder, if available. This is useful for clients + /// implementing a message cache that want to track the folder after it is renamed by another + /// client. + /// This property will only be available if the server supports the + /// OBJECTID extension. + /// + /// The unique folder identifier. + string? Id { get; } + /// /// Get whether or not the folder is subscribed. /// /// /// Gets whether or not the folder is subscribed. /// - /// true if the folder is subscribed; otherwise, false. + /// if the folder is subscribed; otherwise, . bool IsSubscribed { get; } /// @@ -149,27 +222,26 @@ public interface IMailFolder : IEnumerable /// /// Gets whether or not the folder is currently open. /// - /// true if the folder is currently open; otherwise, false. + /// if the folder is currently open; otherwise, . bool IsOpen { get; } /// - /// Get whether or not the folder exists. + /// Get whether or not the folder can be opened. /// /// - /// Gets whether or not the folder exists. + /// Gets whether or not the folder can be opened. /// - /// true if the folder exists; otherwise, false. - bool Exists { get; } + /// if the folder can be opened; otherwise, . + bool CanOpen { get; } /// - /// Get whether or not the folder supports mod-sequences. + /// Get whether or not the folder exists. /// /// - /// If mod-sequences are not supported by the folder, then all of the APIs that take a modseq - /// argument will throw and should not be used. + /// Gets whether or not the folder exists. /// - /// true if supports mod-sequences; otherwise, false. - bool SupportsModSeq { get; } + /// if the folder exists; otherwise, . + bool Exists { get; } /// /// Get the highest mod-sequence value of all messages in the mailbox. @@ -211,13 +283,26 @@ public interface IMailFolder : IEnumerable /// The append limit. uint? AppendLimit { get; } + /// + /// Get the size of the folder. + /// + /// + /// Gets the size of the folder in bytes. + /// If the value is not set, then the size is unspecified. + /// + /// The size of the folder, in bytes. + ulong? Size { get; } + /// /// Get the index of the first unread message in the folder. /// /// - /// This value will only be set after the folder has been opened. + /// Gets the index of the first unread message in the folder. + /// This value will only be set after the folder has been opened. + /// A value of -1 indicates that there are no unread messages in the folder or that the server + /// has not provided the index of the first unread message. /// - /// The index of the first unread message. + /// The index of the first unread message or -1 if there are no unread messages in the folder. int FirstUnread { get; } /// @@ -251,9 +336,31 @@ public interface IMailFolder : IEnumerable /// /// Gets the total number of messages in the folder. /// + /// + /// + /// /// The total number of messages. int Count { get; } + /// + /// Get the threading algorithms supported by the folder. + /// + /// + /// Gets the threading algorithms supported by the folder. + /// + /// The supported threading algorithms. + HashSet ThreadingAlgorithms { get; } + + /// + /// Determine whether or not an supports a feature. + /// + /// + /// Determines whether or not an supports a feature. + /// + /// The desired feature. + /// if the feature is supported; otherwise, . + bool Supports (FolderFeature feature); + /// /// Opens the folder using the requested folder access. /// @@ -271,7 +378,40 @@ public interface IMailFolder : IEnumerable /// The last known value. /// The last known list of unique message identifiers. /// The cancellation token. - FolderAccess Open (FolderAccess access, uint uidValidity, ulong highestModSeq, IList uids, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is not a valid value. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// The quick resynchronization feature has not been enabled. + /// + /// + /// The mail store does not support the quick resynchronization feature. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + FolderAccess Open (FolderAccess access, uint uidValidity, ulong highestModSeq, IList uids, CancellationToken cancellationToken = default); /// /// Asynchronously opens the folder using the requested folder access. @@ -290,7 +430,40 @@ public interface IMailFolder : IEnumerable /// The last known value. /// The last known list of unique message identifiers. /// The cancellation token. - Task OpenAsync (FolderAccess access, uint uidValidity, ulong highestModSeq, IList uids, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is not a valid value. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// The quick resynchronization feature has not been enabled. + /// + /// + /// The mail store does not support the quick resynchronization feature. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task OpenAsync (FolderAccess access, uint uidValidity, ulong highestModSeq, IList uids, CancellationToken cancellationToken = default); /// /// Open the folder using the requested folder access. @@ -301,7 +474,34 @@ public interface IMailFolder : IEnumerable /// The state of the folder. /// The requested folder access. /// The cancellation token. - FolderAccess Open (FolderAccess access, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is not a valid value. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + FolderAccess Open (FolderAccess access, CancellationToken cancellationToken = default); /// /// Asynchronously open the folder using the requested folder access. @@ -312,7 +512,34 @@ public interface IMailFolder : IEnumerable /// The state of the folder. /// The requested folder access. /// The cancellation token. - Task OpenAsync (FolderAccess access, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is not a valid value. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task OpenAsync (FolderAccess access, CancellationToken cancellationToken = default); /// /// Close the folder, optionally expunging the messages marked for deletion. @@ -320,9 +547,33 @@ public interface IMailFolder : IEnumerable /// /// Closes the folder, optionally expunging the messages marked for deletion. /// - /// If set to true, expunge. - /// The cancellation token. - void Close (bool expunge = false, CancellationToken cancellationToken = default (CancellationToken)); + /// If set to , expunge. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + void Close (bool expunge = false, CancellationToken cancellationToken = default); /// /// Asynchronously close the folder, optionally expunging the messages marked for deletion. @@ -331,9 +582,33 @@ public interface IMailFolder : IEnumerable /// Asynchronously closes the folder, optionally expunging the messages marked for deletion. /// /// An asynchronous task context. - /// If set to true, expunge. - /// The cancellation token. - Task CloseAsync (bool expunge = false, CancellationToken cancellationToken = default (CancellationToken)); + /// If set to , expunge. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task CloseAsync (bool expunge = false, CancellationToken cancellationToken = default); /// /// Create a new subfolder with the given name. @@ -343,9 +618,39 @@ public interface IMailFolder : IEnumerable /// /// The created folder. /// The name of the folder to create. - /// true if the folder will be used to contain messages; otherwise false. - /// The cancellation token. - IMailFolder Create (string name, bool isMessageFolder, CancellationToken cancellationToken = default (CancellationToken)); + /// if the folder will be used to contain messages; otherwise, . + /// The cancellation token. + /// + /// is . + /// + /// + /// is empty. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is nil, and thus child folders cannot be created. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + IMailFolder? Create (string name, bool isMessageFolder, CancellationToken cancellationToken = default); /// /// Asynchronously create a new subfolder with the given name. @@ -355,9 +660,39 @@ public interface IMailFolder : IEnumerable /// /// The created folder. /// The name of the folder to create. - /// true if the folder will be used to contain messages; otherwise false. - /// The cancellation token. - Task CreateAsync (string name, bool isMessageFolder, CancellationToken cancellationToken = default (CancellationToken)); + /// if the folder will be used to contain messages; otherwise, . + /// The cancellation token. + /// + /// is . + /// + /// + /// is empty. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is nil, and thus child folders cannot be created. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task CreateAsync (string name, bool isMessageFolder, CancellationToken cancellationToken = default); /// /// Create a new subfolder with the given name. @@ -369,7 +704,42 @@ public interface IMailFolder : IEnumerable /// The name of the folder to create. /// A list of special uses for the folder being created. /// The cancellation token. - IMailFolder Create (string name, IEnumerable specialUses, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is . + /// -or- + /// is . + /// + /// + /// is empty. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is nil, and thus child folders cannot be created. + /// + /// + /// The does not support the creation of special folders. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + IMailFolder? Create (string name, IEnumerable specialUses, CancellationToken cancellationToken = default); /// /// Asynchronously create a new subfolder with the given name. @@ -381,7 +751,42 @@ public interface IMailFolder : IEnumerable /// The name of the folder to create. /// A list of special uses for the folder being created. /// The cancellation token. - Task CreateAsync (string name, IEnumerable specialUses, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is . + /// -or- + /// is . + /// + /// + /// is empty. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is nil, and thus child folders cannot be created. + /// + /// + /// The does not support the creation of special folders. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task CreateAsync (string name, IEnumerable specialUses, CancellationToken cancellationToken = default); /// /// Create a new subfolder with the given name. @@ -393,7 +798,40 @@ public interface IMailFolder : IEnumerable /// The name of the folder to create. /// The special use for the folder being created. /// The cancellation token. - IMailFolder Create (string name, SpecialFolder specialUse, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is . + /// + /// + /// is empty. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is nil, and thus child folders cannot be created. + /// + /// + /// The does not support the creation of special folders. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + IMailFolder? Create (string name, SpecialFolder specialUse, CancellationToken cancellationToken = default); /// /// Asynchronously create a new subfolder with the given name. @@ -405,7 +843,40 @@ public interface IMailFolder : IEnumerable /// The name of the folder to create. /// The special use for the folder being created. /// The cancellation token. - Task CreateAsync (string name, SpecialFolder specialUse, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is . + /// + /// + /// is empty. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is nil, and thus child folders cannot be created. + /// + /// + /// The does not support the creation of special folders. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task CreateAsync (string name, SpecialFolder specialUse, CancellationToken cancellationToken = default); /// /// Rename the folder. @@ -416,7 +887,44 @@ public interface IMailFolder : IEnumerable /// The new parent folder. /// The new name of the folder. /// The cancellation token. - void Rename (IMailFolder parent, string name, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is . + /// -or- + /// is . + /// + /// + /// does not belong to the . + /// -or- + /// is not a legal folder name. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder cannot be renamed (it is either a namespace or the Inbox). + /// + /// + /// The does not exist. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + void Rename (IMailFolder parent, string name, CancellationToken cancellationToken = default); /// /// Asynchronously rename the folder. @@ -428,7 +936,44 @@ public interface IMailFolder : IEnumerable /// The new parent folder. /// The new name of the folder. /// The cancellation token. - Task RenameAsync (IMailFolder parent, string name, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is . + /// -or- + /// is . + /// + /// + /// does not belong to the . + /// -or- + /// is not a legal folder name. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder cannot be renamed (it is either a namespace or the Inbox). + /// + /// + /// The does not exist. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task RenameAsync (IMailFolder parent, string name, CancellationToken cancellationToken = default); /// /// Delete the folder. @@ -437,7 +982,34 @@ public interface IMailFolder : IEnumerable /// Deletes the folder. /// /// The cancellation token. - void Delete (CancellationToken cancellationToken = default (CancellationToken)); + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder cannot be deleted (it is either a namespace or the Inbox). + /// + /// + /// The does not exist. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + void Delete (CancellationToken cancellationToken = default); /// /// Asynchronously delete the folder. @@ -447,7 +1019,34 @@ public interface IMailFolder : IEnumerable /// /// An asynchronous task context. /// The cancellation token. - Task DeleteAsync (CancellationToken cancellationToken = default (CancellationToken)); + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder cannot be deleted (it is either a namespace or the Inbox). + /// + /// + /// The does not exist. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task DeleteAsync (CancellationToken cancellationToken = default); /// /// Subscribe to the folder. @@ -456,7 +1055,28 @@ public interface IMailFolder : IEnumerable /// Subscribes to the folder. /// /// The cancellation token. - void Subscribe (CancellationToken cancellationToken = default (CancellationToken)); + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + void Subscribe (CancellationToken cancellationToken = default); /// /// Asynchronously subscribe to the folder. @@ -466,7 +1086,34 @@ public interface IMailFolder : IEnumerable /// /// An asynchronous task context. /// The cancellation token. - Task SubscribeAsync (CancellationToken cancellationToken = default (CancellationToken)); + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder cannot be deleted (it is either a namespace or the Inbox). + /// + /// + /// The does not exist. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task SubscribeAsync (CancellationToken cancellationToken = default); /// /// Unsubscribe from the folder. @@ -475,7 +1122,28 @@ public interface IMailFolder : IEnumerable /// Unsubscribes from the folder. /// /// The cancellation token. - void Unsubscribe (CancellationToken cancellationToken = default (CancellationToken)); + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + void Unsubscribe (CancellationToken cancellationToken = default); /// /// Asynchronously unsubscribe from the folder. @@ -485,7 +1153,28 @@ public interface IMailFolder : IEnumerable /// /// An asynchronous task context. /// The cancellation token. - Task UnsubscribeAsync (CancellationToken cancellationToken = default (CancellationToken)); + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task UnsubscribeAsync (CancellationToken cancellationToken = default); /// /// Get the subfolders. @@ -499,9 +1188,30 @@ public interface IMailFolder : IEnumerable /// /// The subfolders. /// The status items to pre-populate. - /// If set to true, only subscribed folders will be listed. - /// The cancellation token. - IEnumerable GetSubfolders (StatusItems items, bool subscribedOnly = false, CancellationToken cancellationToken = default (CancellationToken)); + /// If set to , only subscribed folders will be listed. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + IList GetSubfolders (StatusItems items, bool subscribedOnly = false, CancellationToken cancellationToken = default); /// /// Asynchronously get the subfolders. @@ -514,9 +1224,30 @@ public interface IMailFolder : IEnumerable /// /// The subfolders. /// The status items to pre-populate. - /// If set to true, only subscribed folders will be listed. - /// The cancellation token. - Task> GetSubfoldersAsync (StatusItems items, bool subscribedOnly = false, CancellationToken cancellationToken = default (CancellationToken)); + /// If set to , only subscribed folders will be listed. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task> GetSubfoldersAsync (StatusItems items, bool subscribedOnly = false, CancellationToken cancellationToken = default); /// /// Get the subfolders. @@ -525,9 +1256,30 @@ public interface IMailFolder : IEnumerable /// Gets the subfolders. /// /// The subfolders. - /// If set to true, only subscribed folders will be listed. - /// The cancellation token. - IEnumerable GetSubfolders (bool subscribedOnly = false, CancellationToken cancellationToken = default (CancellationToken)); + /// If set to , only subscribed folders will be listed. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + IList GetSubfolders (bool subscribedOnly = false, CancellationToken cancellationToken = default); /// /// Asynchronously get the subfolders. @@ -536,9 +1288,30 @@ public interface IMailFolder : IEnumerable /// Asynchronously gets the subfolders. /// /// The subfolders. - /// If set to true, only subscribed folders will be listed. - /// The cancellation token. - Task> GetSubfoldersAsync (bool subscribedOnly = false, CancellationToken cancellationToken = default (CancellationToken)); + /// If set to , only subscribed folders will be listed. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task> GetSubfoldersAsync (bool subscribedOnly = false, CancellationToken cancellationToken = default); /// /// Get the specified subfolder. @@ -549,7 +1322,37 @@ public interface IMailFolder : IEnumerable /// The subfolder. /// The name of the subfolder. /// The cancellation token. - IMailFolder GetSubfolder (string name, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is . + /// + /// + /// is either an empty string or contains the . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The requested folder could not be found. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + IMailFolder GetSubfolder (string name, CancellationToken cancellationToken = default); /// /// Asynchronously get the specified subfolder. @@ -560,7 +1363,37 @@ public interface IMailFolder : IEnumerable /// The subfolder. /// The name of the subfolder. /// The cancellation token. - Task GetSubfolderAsync (string name, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is . + /// + /// + /// is either an empty string or contains the . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The requested folder could not be found. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task GetSubfolderAsync (string name, CancellationToken cancellationToken = default); /// /// Force the server to flush its state for the folder. @@ -569,7 +1402,31 @@ public interface IMailFolder : IEnumerable /// Forces the server to flush its state for the folder. /// /// The cancellation token. - void Check (CancellationToken cancellationToken = default (CancellationToken)); + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + void Check (CancellationToken cancellationToken = default); /// /// Asynchronously force the server to flush its state for the folder. @@ -579,7 +1436,31 @@ public interface IMailFolder : IEnumerable /// /// An asynchronous task context. /// The cancellation token. - Task CheckAsync (CancellationToken cancellationToken = default (CancellationToken)); + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task CheckAsync (CancellationToken cancellationToken = default); /// /// Update the values of the specified items. @@ -596,7 +1477,34 @@ public interface IMailFolder : IEnumerable /// /// The items to update. /// The cancellation token. - void Status (StatusItems items, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// The mail store does not support the STATUS command. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + void Status (StatusItems items, CancellationToken cancellationToken = default); /// /// Asynchronously update the values of the specified items. @@ -614,7 +1522,34 @@ public interface IMailFolder : IEnumerable /// An asynchronous task context. /// The items to update. /// The cancellation token. - Task StatusAsync (StatusItems items, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// The mail store does not support the STATUS command. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task StatusAsync (StatusItems items, CancellationToken cancellationToken = default); /// /// Get the complete access control list for the folder. @@ -624,7 +1559,31 @@ public interface IMailFolder : IEnumerable /// /// The access control list. /// The cancellation token. - AccessControlList GetAccessControlList (CancellationToken cancellationToken = default (CancellationToken)); + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The mail store does not support the ACL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + AccessControlList GetAccessControlList (CancellationToken cancellationToken = default); /// /// Asynchronously get the complete access control list for the folder. @@ -634,7 +1593,31 @@ public interface IMailFolder : IEnumerable /// /// The access control list. /// The cancellation token. - Task GetAccessControlListAsync (CancellationToken cancellationToken = default (CancellationToken)); + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The mail store does not support the ACL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task GetAccessControlListAsync (CancellationToken cancellationToken = default); /// /// Get the access rights for a particular identifier. @@ -645,7 +1628,34 @@ public interface IMailFolder : IEnumerable /// The access rights. /// The identifier name. /// The cancellation token. - AccessRights GetAccessRights (string name, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The mail store does not support the ACL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + AccessRights GetAccessRights (string name, CancellationToken cancellationToken = default); /// /// Asynchronously get the access rights for a particular identifier. @@ -656,7 +1666,34 @@ public interface IMailFolder : IEnumerable /// The access rights. /// The identifier name. /// The cancellation token. - Task GetAccessRightsAsync (string name, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The mail store does not support the ACL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task GetAccessRightsAsync (string name, CancellationToken cancellationToken = default); /// /// Get the access rights for the current authenticated user. @@ -666,7 +1703,31 @@ public interface IMailFolder : IEnumerable /// /// The access rights. /// The cancellation token. - AccessRights GetMyAccessRights (CancellationToken cancellationToken = default (CancellationToken)); + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The mail store does not support the ACL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + AccessRights GetMyAccessRights (CancellationToken cancellationToken = default); /// /// Asynchronously get the access rights for the current authenticated user. @@ -676,7 +1737,31 @@ public interface IMailFolder : IEnumerable /// /// The access rights. /// The cancellation token. - Task GetMyAccessRightsAsync (CancellationToken cancellationToken = default (CancellationToken)); + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The mail store does not support the ACL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task GetMyAccessRightsAsync (CancellationToken cancellationToken = default); /// /// Add access rights for the specified identity. @@ -687,7 +1772,36 @@ public interface IMailFolder : IEnumerable /// The identity name. /// The access rights. /// The cancellation token. - void AddAccessRights (string name, AccessRights rights, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is . + /// -or- + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The mail store does not support the ACL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + void AddAccessRights (string name, AccessRights rights, CancellationToken cancellationToken = default); /// /// Asynchronously add access rights for the specified identity. @@ -699,7 +1813,36 @@ public interface IMailFolder : IEnumerable /// The identity name. /// The access rights. /// The cancellation token. - Task AddAccessRightsAsync (string name, AccessRights rights, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is . + /// -or- + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The mail store does not support the ACL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task AddAccessRightsAsync (string name, AccessRights rights, CancellationToken cancellationToken = default); /// /// Remove access rights for the specified identity. @@ -710,7 +1853,36 @@ public interface IMailFolder : IEnumerable /// The identity name. /// The access rights. /// The cancellation token. - void RemoveAccessRights (string name, AccessRights rights, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is . + /// -or- + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The mail store does not support the ACL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + void RemoveAccessRights (string name, AccessRights rights, CancellationToken cancellationToken = default); /// /// Asynchronously remove access rights for the specified identity. @@ -722,7 +1894,36 @@ public interface IMailFolder : IEnumerable /// The identity name. /// The access rights. /// The cancellation token. - Task RemoveAccessRightsAsync (string name, AccessRights rights, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is . + /// -or- + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The mail store does not support the ACL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task RemoveAccessRightsAsync (string name, AccessRights rights, CancellationToken cancellationToken = default); /// /// Set the access rights for the specified identity. @@ -733,10 +1934,39 @@ public interface IMailFolder : IEnumerable /// The identity name. /// The access rights. /// The cancellation token. - void SetAccessRights (string name, AccessRights rights, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously set the access rights for the sepcified identity. + /// + /// is . + /// -or- + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The mail store does not support the ACL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + void SetAccessRights (string name, AccessRights rights, CancellationToken cancellationToken = default); + + /// + /// Asynchronously set the access rights for the specified identity. /// /// /// Asynchronously sets the access rights for the specified identity. @@ -745,7 +1975,36 @@ public interface IMailFolder : IEnumerable /// The identity name. /// The access rights. /// The cancellation token. - Task SetAccessRightsAsync (string name, AccessRights rights, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is . + /// -or- + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The mail store does not support the ACL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task SetAccessRightsAsync (string name, AccessRights rights, CancellationToken cancellationToken = default); /// /// Remove all access rights for the given identity. @@ -755,7 +2014,34 @@ public interface IMailFolder : IEnumerable /// /// The identity name. /// The cancellation token. - void RemoveAccess (string name, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The mail store does not support the ACL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + void RemoveAccess (string name, CancellationToken cancellationToken = default); /// /// Asynchronously remove all access rights for the given identity. @@ -766,7 +2052,34 @@ public interface IMailFolder : IEnumerable /// An asynchronous task context. /// The identity name. /// The cancellation token. - Task RemoveAccessAsync (string name, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The mail store does not support the ACL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task RemoveAccessAsync (string name, CancellationToken cancellationToken = default); /// /// Get the quota information for the folder. @@ -778,7 +2091,31 @@ public interface IMailFolder : IEnumerable /// /// The folder quota. /// The cancellation token. - FolderQuota GetQuota (CancellationToken cancellationToken = default (CancellationToken)); + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The mail store does not support quotas. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + FolderQuota GetQuota (CancellationToken cancellationToken = default); /// /// Asynchronously get the quota information for the folder. @@ -790,7 +2127,31 @@ public interface IMailFolder : IEnumerable /// /// The folder quota. /// The cancellation token. - Task GetQuotaAsync (CancellationToken cancellationToken = default (CancellationToken)); + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The mail store does not support quotas. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task GetQuotaAsync (CancellationToken cancellationToken = default); /// /// Set the quota limits for the folder. @@ -801,10 +2162,34 @@ public interface IMailFolder : IEnumerable /// property. /// /// The updated folder quota. - /// If not null, sets the maximum number of messages to allow. - /// If not null, sets the maximum storage size (in kilobytes). - /// The cancellation token. - FolderQuota SetQuota (uint? messageLimit, uint? storageLimit, CancellationToken cancellationToken = default (CancellationToken)); + /// If not , sets the maximum number of messages to allow. + /// If not , sets the maximum storage size (in kilobytes). + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The mail store does not support quotas. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + FolderQuota SetQuota (uint? messageLimit, uint? storageLimit, CancellationToken cancellationToken = default); /// /// Asynchronously set the quota limits for the folder. @@ -815,10 +2200,34 @@ public interface IMailFolder : IEnumerable /// property. /// /// The updated folder quota. - /// If not null, sets the maximum number of messages to allow. - /// If not null, sets the maximum storage size (in kilobytes). - /// The cancellation token. - Task SetQuotaAsync (uint? messageLimit, uint? storageLimit, CancellationToken cancellationToken = default (CancellationToken)); + /// If not , sets the maximum number of messages to allow. + /// If not , sets the maximum storage size (in kilobytes). + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The mail store does not support quotas. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task SetQuotaAsync (uint? messageLimit, uint? storageLimit, CancellationToken cancellationToken = default); /// /// Gets the specified metadata. @@ -829,7 +2238,31 @@ public interface IMailFolder : IEnumerable /// The requested metadata value. /// The metadata tag. /// The cancellation token. - string GetMetadata (MetadataTag tag, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder does not support metadata. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + string? GetMetadata (MetadataTag tag, CancellationToken cancellationToken = default); /// /// Asynchronously gets the specified metadata. @@ -840,7 +2273,31 @@ public interface IMailFolder : IEnumerable /// The requested metadata value. /// The metadata tag. /// The cancellation token. - Task GetMetadataAsync (MetadataTag tag, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder does not support metadata. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task GetMetadataAsync (MetadataTag tag, CancellationToken cancellationToken = default); /// /// Gets the specified metadata. @@ -851,7 +2308,34 @@ public interface IMailFolder : IEnumerable /// The requested metadata. /// The metadata tags. /// The cancellation token. - MetadataCollection GetMetadata (IEnumerable tags, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder does not support metadata. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + MetadataCollection GetMetadata (IEnumerable tags, CancellationToken cancellationToken = default); /// /// Asynchronously gets the specified metadata. @@ -862,7 +2346,34 @@ public interface IMailFolder : IEnumerable /// The requested metadata. /// The metadata tags. /// The cancellation token. - Task GetMetadataAsync (IEnumerable tags, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder does not support metadata. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task GetMetadataAsync (IEnumerable tags, CancellationToken cancellationToken = default); /// /// Gets the specified metadata. @@ -874,7 +2385,36 @@ public interface IMailFolder : IEnumerable /// The metadata options. /// The metadata tags. /// The cancellation token. - MetadataCollection GetMetadata (MetadataOptions options, IEnumerable tags, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is . + /// -or- + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder does not support metadata. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + MetadataCollection GetMetadata (MetadataOptions options, IEnumerable tags, CancellationToken cancellationToken = default); /// /// Asynchronously gets the specified metadata. @@ -886,7 +2426,36 @@ public interface IMailFolder : IEnumerable /// The metadata options. /// The metadata tags. /// The cancellation token. - Task GetMetadataAsync (MetadataOptions options, IEnumerable tags, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is . + /// -or- + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder does not support metadata. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task GetMetadataAsync (MetadataOptions options, IEnumerable tags, CancellationToken cancellationToken = default); /// /// Sets the specified metadata. @@ -896,7 +2465,34 @@ public interface IMailFolder : IEnumerable /// /// The metadata. /// The cancellation token. - void SetMetadata (MetadataCollection metadata, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder does not support metadata. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + void SetMetadata (MetadataCollection metadata, CancellationToken cancellationToken = default); /// /// Asynchronously sets the specified metadata. @@ -907,7 +2503,34 @@ public interface IMailFolder : IEnumerable /// An asynchronous task context. /// The metadata. /// The cancellation token. - Task SetMetadataAsync (MetadataCollection metadata, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder does not support metadata. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task SetMetadataAsync (MetadataCollection metadata, CancellationToken cancellationToken = default); /// /// Expunge the folder, permanently removing all messages marked for deletion. @@ -922,7 +2545,31 @@ public interface IMailFolder : IEnumerable /// event. /// /// The cancellation token. - void Expunge (CancellationToken cancellationToken = default (CancellationToken)); + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + void Expunge (CancellationToken cancellationToken = default); /// /// Asynchronously expunge the folder, permanently removing all messages marked for deletion. @@ -938,7 +2585,31 @@ public interface IMailFolder : IEnumerable /// /// An asynchronous task context. /// The cancellation token. - Task ExpungeAsync (CancellationToken cancellationToken = default (CancellationToken)); + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task ExpungeAsync (CancellationToken cancellationToken = default); /// /// Expunge the specified uids, permanently removing them from the folder. @@ -954,7 +2625,37 @@ public interface IMailFolder : IEnumerable /// /// The message uids. /// The cancellation token. - void Expunge (IList uids, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + void Expunge (IList uids, CancellationToken cancellationToken = default); /// /// Asynchronously expunge the specified uids, permanently removing them from the folder. @@ -971,231 +2672,857 @@ public interface IMailFolder : IEnumerable /// An asynchronous task context. /// The message uids. /// The cancellation token. - Task ExpungeAsync (IList uids, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Append the specified message to the folder. - /// - /// - /// Appends the specified message to the folder and returns the UniqueId assigned to the message. - /// - /// The UID of the appended message, if available; otherwise, null. - /// The message. - /// The message flags. - /// The cancellation token. - /// The progress reporting mechanism. - UniqueId? Append (MimeMessage message, MessageFlags flags = MessageFlags.None, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Asynchronously append the specified message to the folder. - /// - /// - /// Asynchronously appends the specified message to the folder and returns the UniqueId assigned to the message. - /// - /// The UID of the appended message, if available; otherwise, null. - /// The message. - /// The message flags. - /// The cancellation token. - /// The progress reporting mechanism. - Task AppendAsync (MimeMessage message, MessageFlags flags = MessageFlags.None, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Append the specified message to the folder. - /// - /// - /// Appends the specified message to the folder and returns the UniqueId assigned to the message. - /// - /// The UID of the appended message, if available; otherwise, null. - /// The message. - /// The message flags. - /// The received date of the message. - /// The cancellation token. - /// The progress reporting mechanism. - UniqueId? Append (MimeMessage message, MessageFlags flags, DateTimeOffset date, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Asynchronously append the specified message to the folder. - /// - /// - /// Asynchronously appends the specified message to the folder and returns the UniqueId assigned to the message. - /// - /// The UID of the appended message, if available; otherwise, null. - /// The message. - /// The message flags. - /// The received date of the message. - /// The cancellation token. - /// The progress reporting mechanism. - Task AppendAsync (MimeMessage message, MessageFlags flags, DateTimeOffset date, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Append the specified message to the folder. - /// - /// - /// Appends the specified message to the folder and returns the UniqueId assigned to the message. - /// - /// The UID of the appended message, if available; otherwise, null. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task ExpungeAsync (IList uids, CancellationToken cancellationToken = default); + + /// + /// Append a message to the folder. + /// + /// + /// Appends a message to the folder and returns the UniqueId assigned to the message. + /// + /// The UID of the appended message, if available; otherwise, . + /// The append request. + /// The cancellation token. + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// -or- + /// The request included annotations but the folder does not support annotations. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + UniqueId? Append (IAppendRequest request, CancellationToken cancellationToken = default); + + /// + /// Asynchronously append a message to the folder. + /// + /// + /// Asynchronously appends a message to the folder and returns the UniqueId assigned to the message. + /// + /// The UID of the appended message, if available; otherwise, . + /// The append request. + /// The cancellation token. + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// -or- + /// The request included annotations but the folder does not support annotations. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task AppendAsync (IAppendRequest request, CancellationToken cancellationToken = default); + + /// + /// Append a message to the folder. + /// + /// + /// Appends a message to the folder and returns the UniqueId assigned to the message. + /// + /// The UID of the appended message, if available; otherwise, . /// The formatting options. - /// The message. - /// The message flags. - /// The cancellation token. - /// The progress reporting mechanism. - UniqueId? Append (FormatOptions options, MimeMessage message, MessageFlags flags = MessageFlags.None, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Asynchronously append the specified message to the folder. - /// - /// - /// Asynchronously appends the specified message to the folder and returns the UniqueId assigned to the message. - /// - /// The UID of the appended message, if available; otherwise, null. + /// The append request. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// -or- + /// The request included annotations but the folder does not support annotations. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + UniqueId? Append (FormatOptions options, IAppendRequest request, CancellationToken cancellationToken = default); + + /// + /// Asynchronously append a message to the folder. + /// + /// + /// Asynchronously appends a message to the folder and returns the UniqueId assigned to the message. + /// + /// The UID of the appended message, if available; otherwise, . /// The formatting options. - /// The message. - /// The message flags. - /// The cancellation token. - /// The progress reporting mechanism. - Task AppendAsync (FormatOptions options, MimeMessage message, MessageFlags flags = MessageFlags.None, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Append the specified message to the folder. - /// - /// - /// Appends the specified message to the folder and returns the UniqueId assigned to the message. - /// - /// The UID of the appended message, if available; otherwise, null. + /// The append request. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// -or- + /// The request included annotations but the folder does not support annotations. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task AppendAsync (FormatOptions options, IAppendRequest request, CancellationToken cancellationToken = default); + + /// + /// Append multiple messages to the folder. + /// + /// + /// Appends multiple messages to the folder and returns the UniqueIds assigned to each of the messages. + /// + /// The UIDs of the appended messages, if available; otherwise, an empty array. + /// The append requests. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// -or- + /// One ore more requests included annotations but the folder does not support annotations. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + IList Append (IList requests, CancellationToken cancellationToken = default); + + /// + /// Asynchronously append multiple messages to the folder. + /// + /// + /// Asynchronously appends multiple messages to the folder and returns the UniqueIds assigned to each of the messages. + /// + /// The UID of the appended message, if available; otherwise, an empty array. + /// The append requests. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// -or- + /// One ore more requests included annotations but the folder does not support annotations. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task> AppendAsync (IList requests, CancellationToken cancellationToken = default); + + /// + /// Append multiple messages to the folder. + /// + /// + /// Appends multiple messages to the folder and returns the UniqueIds assigned to each of the messages. + /// + /// The UIDs of the appended messages, if available; otherwise, an empty array. /// The formatting options. - /// The message. - /// The message flags. - /// The received date of the message. - /// The cancellation token. - /// The progress reporting mechanism. - UniqueId? Append (FormatOptions options, MimeMessage message, MessageFlags flags, DateTimeOffset date, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Asynchronously append the specified message to the folder. - /// - /// - /// Asynchronously appends the specified message to the folder and returns the UniqueId assigned to the message. - /// - /// The UID of the appended message, if available; otherwise, null. + /// The append requests. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// -or- + /// One ore more requests included annotations but the folder does not support annotations. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + IList Append (FormatOptions options, IList requests, CancellationToken cancellationToken = default); + + /// + /// Asynchronously append multiple messages to the folder. + /// + /// + /// Asynchronously appends multiple messages to the folder and returns the UniqueIds assigned to each of the messages. + /// + /// The UID of the appended message, if available; otherwise, an empty array. /// The formatting options. - /// The message. - /// The message flags. - /// The received date of the message. - /// The cancellation token. - /// The progress reporting mechanism. - Task AppendAsync (FormatOptions options, MimeMessage message, MessageFlags flags, DateTimeOffset date, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Append the specified messages to the folder. - /// - /// - /// Appends the specified messages to the folder and returns the UniqueIds assigned to the messages. - /// - /// The UIDs of the appended messages, if available; otherwise an empty array. - /// The list of messages to append to the folder. - /// The message flags to use for each message. - /// The cancellation token. - /// The progress reporting mechanism. - IList Append (IList messages, IList flags, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Asynchronously append the specified messages to the folder. - /// - /// - /// Asynchronously appends the specified messages to the folder and returns the UniqueIds assigned to the messages. - /// - /// The UIDs of the appended messages, if available; otherwise an empty array. - /// The list of messages to append to the folder. - /// The message flags to use for each message. - /// The cancellation token. - /// The progress reporting mechanism. - Task> AppendAsync (IList messages, IList flags, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Append the specified messages to the folder. - /// - /// - /// Appends the specified messages to the folder and returns the UniqueIds assigned to the messages. - /// - /// The UIDs of the appended messages, if available; otherwise an empty array. - /// The list of messages to append to the folder. - /// The message flags to use for each of the messages. - /// The received dates to use for each of the messages. - /// The cancellation token. - /// The progress reporting mechanism. - IList Append (IList messages, IList flags, IList dates, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Asynchronously append the specified messages to the folder. - /// - /// - /// Asynchronously appends the specified messages to the folder and returns the UniqueIds assigned to the messages. - /// - /// The UIDs of the appended messages, if available; otherwise an empty array. - /// The list of messages to append to the folder. - /// The message flags to use for each of the messages. - /// The received dates to use for each of the messages. - /// The cancellation token. - /// The progress reporting mechanism. - Task> AppendAsync (IList messages, IList flags, IList dates, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Append the specified messages to the folder. - /// - /// - /// Appends the specified messages to the folder and returns the UniqueIds assigned to the messages. - /// - /// The UIDs of the appended messages, if available; otherwise an empty array. + /// The append requests. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// -or- + /// One ore more requests included annotations but the folder does not support annotations. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task> AppendAsync (FormatOptions options, IList requests, CancellationToken cancellationToken = default); + + /// + /// Replace a message in the folder. + /// + /// + /// Replaces a message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, . + /// The UID of the message to be replaced. + /// The replace request. + /// The cancellation token. + /// + /// is . + /// + /// + /// is invalid. + /// -or- + /// The destination folder does not belong to this . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + UniqueId? Replace (UniqueId uid, IReplaceRequest request, CancellationToken cancellationToken = default); + + /// + /// Asynchronously replace a message in the folder. + /// + /// + /// Asynchronously replaces a message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the appended message, if available; otherwise, . + /// The UID of the message to be replaced. + /// The replace request. + /// The cancellation token. + /// + /// is . + /// + /// + /// is invalid. + /// -or- + /// The destination folder does not belong to this . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + Task ReplaceAsync (UniqueId uid, IReplaceRequest request, CancellationToken cancellationToken = default); + + /// + /// Replace a message in the folder. + /// + /// + /// Replaces a message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, . /// The formatting options. - /// The list of messages to append to the folder. - /// The message flags to use for each message. - /// The cancellation token. - /// The progress reporting mechanism. - IList Append (FormatOptions options, IList messages, IList flags, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Asynchronously append the specified messages to the folder. - /// - /// - /// Asynchronously appends the specified messages to the folder and returns the UniqueIds assigned to the messages. - /// - /// The UIDs of the appended messages, if available; otherwise an empty array. + /// The UID of the message to be replaced. + /// The replace request. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// is invalid. + /// -or- + /// The destination folder does not belong to this . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + UniqueId? Replace (FormatOptions options, UniqueId uid, IReplaceRequest request, CancellationToken cancellationToken = default); + + /// + /// Asynchronously replace a message in the folder. + /// + /// + /// Asynchronously replaces a message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, . /// The formatting options. - /// The list of messages to append to the folder. - /// The message flags to use for each message. - /// The cancellation token. - /// The progress reporting mechanism. - Task> AppendAsync (FormatOptions options, IList messages, IList flags, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Append the specified messages to the folder. - /// - /// - /// Appends the specified messages to the folder and returns the UniqueIds assigned to the messages. - /// - /// The UIDs of the appended messages, if available; otherwise an empty array. + /// The UID of the message to be replaced. + /// The replace request. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// is invalid. + /// -or- + /// The destination folder does not belong to this . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + Task ReplaceAsync (FormatOptions options, UniqueId uid, IReplaceRequest request, CancellationToken cancellationToken = default); + + /// + /// Replace a message in the folder. + /// + /// + /// Replaces a message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, . + /// The index of the message to be replaced. + /// The replace request. + /// The cancellation token. + /// + /// is . + /// + /// + /// is out of range. + /// + /// + /// The destination folder does not belong to this . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + UniqueId? Replace (int index, IReplaceRequest request, CancellationToken cancellationToken = default); + + /// + /// Asynchronously replace a message in the folder. + /// + /// + /// Asynchronously replaces a message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the appended message, if available; otherwise, . + /// The index of the message to be replaced. + /// The replace request. + /// The cancellation token. + /// + /// is . + /// + /// + /// is out of range. + /// + /// + /// The destination folder does not belong to this . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + Task ReplaceAsync (int index, IReplaceRequest request, CancellationToken cancellationToken = default); + + /// + /// Replace a message in the folder. + /// + /// + /// Replaces a message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, . /// The formatting options. - /// The list of messages to append to the folder. - /// The message flags to use for each of the messages. - /// The received dates to use for each of the messages. - /// The cancellation token. - /// The progress reporting mechanism. - IList Append (FormatOptions options, IList messages, IList flags, IList dates, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Asynchronously append the specified messages to the folder. - /// - /// - /// Asynchronously appends the specified messages to the folder and returns the UniqueIds assigned to the messages. - /// - /// The UIDs of the appended messages, if available; otherwise an empty array. + /// The index of the message to be replaced. + /// The replace request. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// is out of range. + /// + /// + /// The destination folder does not belong to this . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + UniqueId? Replace (FormatOptions options, int index, IReplaceRequest request, CancellationToken cancellationToken = default); + + /// + /// Asynchronously replace a message in the folder. + /// + /// + /// Asynchronously replaces a message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, . /// The formatting options. - /// The list of messages to append to the folder. - /// The message flags to use for each of the messages. - /// The received dates to use for each of the messages. - /// The cancellation token. - /// The progress reporting mechanism. - Task> AppendAsync (FormatOptions options, IList messages, IList flags, IList dates, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + /// The index of the message to be replaced. + /// The replace request. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// is out of range. + /// + /// + /// The destination folder does not belong to this . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + Task ReplaceAsync (FormatOptions options, int index, IReplaceRequest request, CancellationToken cancellationToken = default); /// /// Copy the specified message to the destination folder. @@ -1203,11 +3530,46 @@ public interface IMailFolder : IEnumerable /// /// Copies the specified message to the destination folder. /// - /// The UID of the message in the destination folder, if available; otherwise, null. + /// The UID of the message in the destination folder, if available; otherwise, . /// The UID of the message to copy. /// The destination folder. /// The cancellation token. - UniqueId? CopyTo (UniqueId uid, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is . + /// + /// + /// is invalid. + /// -or- + /// The destination folder does not belong to the . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The mail store does not support the UIDPLUS extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + UniqueId? CopyTo (UniqueId uid, IMailFolder destination, CancellationToken cancellationToken = default); /// /// Asynchronously copy the specified message to the destination folder. @@ -1215,11 +3577,46 @@ public interface IMailFolder : IEnumerable /// /// Asynchronously copies the specified message to the destination folder. /// - /// The UID of the message in the destination folder, if available; otherwise, null. + /// The UID of the message in the destination folder, if available; otherwise, . /// The UID of the message to copy. /// The destination folder. /// The cancellation token. - Task CopyToAsync (UniqueId uid, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is . + /// + /// + /// is invalid. + /// -or- + /// The destination folder does not belong to the . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The mail store does not support the UIDPLUS extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task CopyToAsync (UniqueId uid, IMailFolder destination, CancellationToken cancellationToken = default); /// /// Copy the specified messages to the destination folder. @@ -1231,7 +3628,44 @@ public interface IMailFolder : IEnumerable /// The UIDs of the messages to copy. /// The destination folder. /// The cancellation token. - UniqueIdMap CopyTo (IList uids, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// -or- + /// The destination folder does not belong to the . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The mail store does not support the UIDPLUS extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + UniqueIdMap CopyTo (IList uids, IMailFolder destination, CancellationToken cancellationToken = default); /// /// Asynchronously copy the specified messages to the destination folder. @@ -1243,7 +3677,44 @@ public interface IMailFolder : IEnumerable /// The UIDs of the messages to copy. /// The destination folder. /// The cancellation token. - Task CopyToAsync (IList uids, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// -or- + /// The destination folder does not belong to the . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The mail store does not support the UIDPLUS extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task CopyToAsync (IList uids, IMailFolder destination, CancellationToken cancellationToken = default); /// /// Move the specified message to the destination folder. @@ -1251,11 +3722,46 @@ public interface IMailFolder : IEnumerable /// /// Moves the specified message to the destination folder. /// - /// The UID of the message in the destination folder, if available; otherwise, null. + /// The UID of the message in the destination folder, if available; otherwise, . /// The UID of the message to move. /// The destination folder. /// The cancellation token. - UniqueId? MoveTo (UniqueId uid, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is . + /// + /// + /// is invalid. + /// -or- + /// The destination folder does not belong to the . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The mail store does not support the UIDPLUS extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + UniqueId? MoveTo (UniqueId uid, IMailFolder destination, CancellationToken cancellationToken = default); /// /// Asynchronously move the specified message to the destination folder. @@ -1263,11 +3769,46 @@ public interface IMailFolder : IEnumerable /// /// Asynchronously moves the specified message to the destination folder. /// - /// The UID of the message in the destination folder, if available; otherwise, null. + /// The UID of the message in the destination folder, if available; otherwise, . /// The UID of the message to move. /// The destination folder. /// The cancellation token. - Task MoveToAsync (UniqueId uid, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is . + /// + /// + /// is invalid. + /// -or- + /// The destination folder does not belong to the . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The mail store does not support the UIDPLUS extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task MoveToAsync (UniqueId uid, IMailFolder destination, CancellationToken cancellationToken = default); /// /// Move the specified messages to the destination folder. @@ -1279,7 +3820,44 @@ public interface IMailFolder : IEnumerable /// The UIDs of the messages to copy. /// The destination folder. /// The cancellation token. - UniqueIdMap MoveTo (IList uids, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// -or- + /// The destination folder does not belong to the . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The mail store does not support the UIDPLUS extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + UniqueIdMap MoveTo (IList uids, IMailFolder destination, CancellationToken cancellationToken = default); /// /// Asynchronously move the specified messages to the destination folder. @@ -1291,7 +3869,44 @@ public interface IMailFolder : IEnumerable /// The UIDs of the messages to copy. /// The destination folder. /// The cancellation token. - Task MoveToAsync (IList uids, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// -or- + /// The destination folder does not belong to the . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The mail store does not support the UIDPLUS extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task MoveToAsync (IList uids, IMailFolder destination, CancellationToken cancellationToken = default); /// /// Copy the specified message to the destination folder. @@ -1302,7 +3917,40 @@ public interface IMailFolder : IEnumerable /// The index of the message to copy. /// The destination folder. /// The cancellation token. - void CopyTo (int index, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is . + /// + /// + /// does not refer to a valid message index. + /// + /// + /// The destination folder does not belong to the . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + void CopyTo (int index, IMailFolder destination, CancellationToken cancellationToken = default); /// /// Asynchronously copy the specified message to the destination folder. @@ -1314,7 +3962,40 @@ public interface IMailFolder : IEnumerable /// The indexes of the message to copy. /// The destination folder. /// The cancellation token. - Task CopyToAsync (int index, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is . + /// + /// + /// does not refer to a valid message index. + /// + /// + /// The destination folder does not belong to the . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task CopyToAsync (int index, IMailFolder destination, CancellationToken cancellationToken = default); /// /// Copy the specified messages to the destination folder. @@ -1325,7 +4006,41 @@ public interface IMailFolder : IEnumerable /// The indexes of the messages to copy. /// The destination folder. /// The cancellation token. - void CopyTo (IList indexes, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// -or- + /// The destination folder does not belong to the . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + void CopyTo (IList indexes, IMailFolder destination, CancellationToken cancellationToken = default); /// /// Asynchronously copy the specified messages to the destination folder. @@ -1337,7 +4052,41 @@ public interface IMailFolder : IEnumerable /// The indexes of the messages to copy. /// The destination folder. /// The cancellation token. - Task CopyToAsync (IList indexes, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// -or- + /// The destination folder does not belong to the . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task CopyToAsync (IList indexes, IMailFolder destination, CancellationToken cancellationToken = default); /// /// Move the specified message to the destination folder. @@ -1348,7 +4097,40 @@ public interface IMailFolder : IEnumerable /// The index of the message to move. /// The destination folder. /// The cancellation token. - void MoveTo (int index, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is . + /// + /// + /// does not refer to a valid message index. + /// + /// + /// The destination folder does not belong to the . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + void MoveTo (int index, IMailFolder destination, CancellationToken cancellationToken = default); /// /// Asynchronously move the specified message to the destination folder. @@ -1360,7 +4142,40 @@ public interface IMailFolder : IEnumerable /// The index of the message to move. /// The destination folder. /// The cancellation token. - Task MoveToAsync (int index, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is . + /// + /// + /// does not refer to a valid message index. + /// + /// + /// The destination folder does not belong to the . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task MoveToAsync (int index, IMailFolder destination, CancellationToken cancellationToken = default); /// /// Move the specified messages to the destination folder. @@ -1371,7 +4186,41 @@ public interface IMailFolder : IEnumerable /// The indexes of the messages to move. /// The destination folder. /// The cancellation token. - void MoveTo (IList indexes, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// -or- + /// The destination folder does not belong to the . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + void MoveTo (IList indexes, IMailFolder destination, CancellationToken cancellationToken = default); /// /// Asynchronously move the specified messages to the destination folder. @@ -1383,7 +4232,41 @@ public interface IMailFolder : IEnumerable /// The indexes of the messages to move. /// The destination folder. /// The cancellation token. - Task MoveToAsync (IList indexes, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// -or- + /// The destination folder does not belong to the . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task MoveToAsync (IList indexes, IMailFolder destination, CancellationToken cancellationToken = default); /// /// Fetch the message summaries for the specified message UIDs. @@ -1398,13 +4281,45 @@ public interface IMailFolder : IEnumerable /// not requested at all. /// /// - /// + /// /// /// An enumeration of summaries for the requested messages. /// The UIDs. - /// The message summary items to fetch. - /// The cancellation token. - IList Fetch (IList uids, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)); + /// The fetch request. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + IList Fetch (IList uids, IFetchRequest request, CancellationToken cancellationToken = default); /// /// Asynchronously fetch the message summaries for the specified message UIDs. @@ -1421,15 +4336,47 @@ public interface IMailFolder : IEnumerable /// /// An enumeration of summaries for the requested messages. /// The UIDs. - /// The message summary items to fetch. - /// The cancellation token. - Task> FetchAsync (IList uids, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)); + /// The fetch request. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task> FetchAsync (IList uids, IFetchRequest request, CancellationToken cancellationToken = default); /// - /// Fetch the message summaries for the specified message UIDs. + /// Fetch the message summaries for the specified message indexes. /// /// - /// Fetches the message summaries for the specified message UIDs. + /// Fetches the message summaries for the specified message indexes. /// It should be noted that if another client has modified any message /// in the folder, the mail service may choose to return information that was /// not explicitly requested. It is therefore important to be prepared to @@ -1438,18 +4385,49 @@ public interface IMailFolder : IEnumerable /// not requested at all. /// /// An enumeration of summaries for the requested messages. - /// The UIDs. - /// The message summary items to fetch. - /// The desired header fields. - /// The cancellation token. - IList Fetch (IList uids, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)); + /// The indexes. + /// The fetch request. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + IList Fetch (IList indexes, IFetchRequest request, CancellationToken cancellationToken = default); /// - /// Asynchronously fetch the message summaries for the specified message UIDs. + /// Asynchronously fetch the message summaries for the specified message indexes. /// /// /// Asynchronously fetches the message summaries for the specified message - /// UIDs. + /// indexes. /// It should be noted that if another client has modified any message /// in the folder, the mail service may choose to return information that was /// not explicitly requested. It is therefore important to be prepared to @@ -1458,17 +4436,49 @@ public interface IMailFolder : IEnumerable /// not requested at all. /// /// An enumeration of summaries for the requested messages. - /// The UIDs. - /// The message summary items to fetch. - /// The desired header fields. - /// The cancellation token. - Task> FetchAsync (IList uids, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)); + /// The indexes. + /// The fetch request. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task> FetchAsync (IList indexes, IFetchRequest request, CancellationToken cancellationToken = default); /// - /// Fetch the message summaries for the specified message UIDs. + /// Fetch the message summaries for the messages between the two indexes, inclusive. /// /// - /// Fetches the message summaries for the specified message UIDs. + /// Fetches the message summaries for the messages between the two + /// indexes, inclusive. /// It should be noted that if another client has modified any message /// in the folder, the mail service may choose to return information that was /// not explicitly requested. It is therefore important to be prepared to @@ -1477,18 +4487,50 @@ public interface IMailFolder : IEnumerable /// not requested at all. /// /// An enumeration of summaries for the requested messages. - /// The UIDs. - /// The message summary items to fetch. - /// The desired header fields. - /// The cancellation token. - IList Fetch (IList uids, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)); + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// The fetch request. + /// The cancellation token. + /// + /// is . + /// + /// + /// is out of range. + /// -or- + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + IList Fetch (int min, int max, IFetchRequest request, CancellationToken cancellationToken = default); /// - /// Asynchronously fetch the message summaries for the specified message UIDs. + /// Asynchronously fetch the message summaries for the messages between the two indexes, inclusive. /// /// - /// Asynchronously fetches the message summaries for the specified message - /// UIDs. + /// Asynchronously fetches the message summaries for the messages between + /// the two indexes, inclusive. /// It should be noted that if another client has modified any message /// in the folder, the mail service may choose to return information that was /// not explicitly requested. It is therefore important to be prepared to @@ -1497,2655 +4539,3359 @@ public interface IMailFolder : IEnumerable /// not requested at all. /// /// An enumeration of summaries for the requested messages. - /// The UIDs. - /// The message summary items to fetch. - /// The desired header fields. - /// The cancellation token. - Task> FetchAsync (IList uids, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)); + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// The fetch request. + /// The cancellation token. + /// + /// is . + /// + /// + /// is out of range. + /// -or- + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task> FetchAsync (int min, int max, IFetchRequest request, CancellationToken cancellationToken = default); /// - /// Fetch the message summaries for the specified message UIDs that have a - /// higher mod-sequence value than the one specified. + /// Get the specified message headers. /// /// - /// Fetches the message summaries for the specified message UIDs that - /// have a higher mod-sequence value than the one specified. - /// If the mail store supports quick resynchronization and the application has - /// enabled this feature via , - /// then this method will emit events for messages that - /// have vanished since the specified mod-sequence value. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. + /// Gets the specified message headers. /// - /// An enumeration of summaries for the requested messages. - /// The UIDs. - /// The mod-sequence value. - /// The message summary items to fetch. + /// The message headers. + /// The UID of the message. /// The cancellation token. - IList Fetch (IList uids, ulong modseq, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)); + /// The progress reporting mechanism. + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message headers. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + HeaderList GetHeaders (UniqueId uid, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Asynchronously fetch the message summaries for the specified message UIDs that have a - /// higher mod-sequence value than the one specified. + /// Asynchronously get the specified message headers. /// /// - /// Asynchronously fetches the message summaries for the specified message UIDs that - /// have a higher mod-sequence value than the one specified. - /// If the mail store supports quick resynchronization and the application has - /// enabled this feature via , - /// then this method will emit events for messages that - /// have vanished since the specified mod-sequence value. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. + /// Asynchronously gets the specified message headers. /// - /// An enumeration of summaries for the requested messages. - /// The UIDs. - /// The mod-sequence value. - /// The message summary items to fetch. + /// The message headers. + /// The UID of the message. /// The cancellation token. - Task> FetchAsync (IList uids, ulong modseq, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)); + /// The progress reporting mechanism. + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message headers. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task GetHeadersAsync (UniqueId uid, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Fetch the message summaries for the specified message UIDs that have a - /// higher mod-sequence value than the one specified. + /// Get the specified body part headers. /// /// - /// Fetches the message summaries for the specified message UIDs that - /// have a higher mod-sequence value than the one specified. - /// If the mail store supports quick resynchronization and the application has - /// enabled this feature via , - /// then this method will emit events for messages that - /// have vanished since the specified mod-sequence value. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. + /// Gets the specified body part headers. /// - /// An enumeration of summaries for the requested messages. - /// The UIDs. - /// The mod-sequence value. - /// The message summary items to fetch. - /// The desired header fields. - /// The cancellation token. - IList Fetch (IList uids, ulong modseq, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously fetch the message summaries for the specified message UIDs that have a - /// higher mod-sequence value than the one specified. - /// - /// - /// Asynchronously fetches the message summaries for the specified message UIDs that - /// have a higher mod-sequence value than the one specified. - /// If the mail store supports quick resynchronization and the application has - /// enabled this feature via , - /// then this method will emit events for messages that - /// have vanished since the specified mod-sequence value. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. - /// - /// An enumeration of summaries for the requested messages. - /// The UIDs. - /// The mod-sequence value. - /// The message summary items to fetch. - /// The desired header fields. - /// The cancellation token. - Task> FetchAsync (IList uids, ulong modseq, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Fetch the message summaries for the specified message UIDs that have a - /// higher mod-sequence value than the one specified. - /// - /// - /// Fetches the message summaries for the specified message UIDs that - /// have a higher mod-sequence value than the one specified. - /// If the mail store supports quick resynchronization and the application has - /// enabled this feature via , - /// then this method will emit events for messages that - /// have vanished since the specified mod-sequence value. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. - /// - /// An enumeration of summaries for the requested messages. - /// The UIDs. - /// The mod-sequence value. - /// The message summary items to fetch. - /// The desired header fields. - /// The cancellation token. - IList Fetch (IList uids, ulong modseq, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously fetch the message summaries for the specified message UIDs that have a - /// higher mod-sequence value than the one specified. - /// - /// - /// Asynchronously fetches the message summaries for the specified message UIDs that - /// have a higher mod-sequence value than the one specified. - /// If the mail store supports quick resynchronization and the application has - /// enabled this feature via , - /// then this method will emit events for messages that - /// have vanished since the specified mod-sequence value. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. - /// - /// An enumeration of summaries for the requested messages. - /// The UIDs. - /// The mod-sequence value. - /// The message summary items to fetch. - /// The desired header fields. - /// The cancellation token. - Task> FetchAsync (IList uids, ulong modseq, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Fetch the message summaries for the specified message indexes. - /// - /// - /// Fetches the message summaries for the specified message indexes. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. - /// - /// An enumeration of summaries for the requested messages. - /// The indexes. - /// The message summary items to fetch. - /// The cancellation token. - IList Fetch (IList indexes, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously fetch the message summaries for the specified message indexes. - /// - /// - /// Asynchronously fetches the message summaries for the specified message - /// indexes. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. - /// - /// An enumeration of summaries for the requested messages. - /// The indexes. - /// The message summary items to fetch. - /// The cancellation token. - Task> FetchAsync (IList indexes, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Fetch the message summaries for the specified message indexes. - /// - /// - /// Fetches the message summaries for the specified message indexes. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. - /// - /// An enumeration of summaries for the requested messages. - /// The indexes. - /// The message summary items to fetch. - /// THe desired header fields. - /// The cancellation token. - IList Fetch (IList indexes, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously fetch the message summaries for the specified message indexes. - /// - /// - /// Asynchronously fetches the message summaries for the specified message - /// indexes. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. - /// - /// An enumeration of summaries for the requested messages. - /// The indexes. - /// The message summary items to fetch. - /// THe desired header fields. - /// The cancellation token. - Task> FetchAsync (IList indexes, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Fetch the message summaries for the specified message indexes. - /// - /// - /// Fetches the message summaries for the specified message indexes. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. - /// - /// An enumeration of summaries for the requested messages. - /// The indexes. - /// The message summary items to fetch. - /// THe desired header fields. - /// The cancellation token. - IList Fetch (IList indexes, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously fetch the message summaries for the specified message indexes. - /// - /// - /// Asynchronously fetches the message summaries for the specified message - /// indexes. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. - /// - /// An enumeration of summaries for the requested messages. - /// The indexes. - /// The message summary items to fetch. - /// THe desired header fields. - /// The cancellation token. - Task> FetchAsync (IList indexes, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Fetch the message summaries for the specified message indexes that have a - /// higher mod-sequence value than the one specified. - /// - /// - /// Fetches the message summaries for the specified message indexes that - /// have a higher mod-sequence value than the one specified. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. - /// - /// An enumeration of summaries for the requested messages. - /// The indexes. - /// The mod-sequence value. - /// The message summary items to fetch. - /// The cancellation token. - IList Fetch (IList indexes, ulong modseq, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously fetch the message summaries for the specified message indexes that have a - /// higher mod-sequence value than the one specified. - /// - /// - /// Asynchronously fetches the message summaries for the specified message - /// indexes that have a higher mod-sequence value than the one specified. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. - /// - /// An enumeration of summaries for the requested messages. - /// The indexes. - /// The mod-sequence value. - /// The message summary items to fetch. - /// The cancellation token. - Task> FetchAsync (IList indexes, ulong modseq, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Fetch the message summaries for the specified message indexes that have a - /// higher mod-sequence value than the one specified. - /// - /// - /// Fetches the message summaries for the specified message indexes that - /// have a higher mod-sequence value than the one specified. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. - /// - /// An enumeration of summaries for the requested messages. - /// The indexes. - /// The mod-sequence value. - /// The message summary items to fetch. - /// THe desired header fields. - /// The cancellation token. - IList Fetch (IList indexes, ulong modseq, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously fetch the message summaries for the specified message indexes - /// that have a higher mod-sequence value than the one specified. - /// - /// - /// Asynchronously fetches the message summaries for the specified message - /// indexes that have a higher mod-sequence value than the one specified. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. - /// - /// An enumeration of summaries for the requested messages. - /// The indexes. - /// The mod-sequence value. - /// The message summary items to fetch. - /// THe desired header fields. - /// The cancellation token. - Task> FetchAsync (IList indexes, ulong modseq, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Fetch the message summaries for the specified message indexes that - /// have a higher mod-sequence value than the one specified. - /// - /// - /// Fetches the message summaries for the specified message indexes that - /// have a higher mod-sequence value than the one specified. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. - /// - /// An enumeration of summaries for the requested messages. - /// The indexes. - /// The mod-sequence value. - /// The message summary items to fetch. - /// THe desired header fields. - /// The cancellation token. - IList Fetch (IList indexes, ulong modseq, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously fetch the message summaries for the specified message indexes - /// that have a higher mod-sequence value than the one specified. - /// - /// - /// Asynchronously fetches the message summaries for the specified message - /// indexes that have a higher mod-sequence value than the one specified. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. - /// - /// An enumeration of summaries for the requested messages. - /// The indexes. - /// The mod-sequence value. - /// The message summary items to fetch. - /// THe desired header fields. - /// The cancellation token. - Task> FetchAsync (IList indexes, ulong modseq, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Fetch the message summaries for the messages between the two indexes, inclusive. - /// - /// - /// Fetches the message summaries for the messages between the two - /// indexes, inclusive. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. - /// - /// An enumeration of summaries for the requested messages. - /// The minimum index. - /// The maximum index, or -1 to specify no upper bound. - /// The message summary items to fetch. - /// The cancellation token. - IList Fetch (int min, int max, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously fetch the message summaries for the messages between the two indexes, inclusive. - /// - /// - /// Asynchronously fetches the message summaries for the messages between - /// the two indexes, inclusive. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. - /// - /// An enumeration of summaries for the requested messages. - /// The minimum index. - /// The maximum index, or -1 to specify no upper bound. - /// The message summary items to fetch. - /// The cancellation token. - Task> FetchAsync (int min, int max, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Fetch the message summaries for the messages between the two indexes, inclusive. - /// - /// - /// Fetches the message summaries for the messages between the two - /// indexes, inclusive. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. - /// - /// An enumeration of summaries for the requested messages. - /// The minimum index. - /// The maximum index, or -1 to specify no upper bound. - /// The message summary items to fetch. - /// THe desired header fields. - /// The cancellation token. - IList Fetch (int min, int max, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously fetch the message summaries for the messages between the two indexes, inclusive. - /// - /// - /// Asynchronously fetches the message summaries for the messages between - /// the two indexes, inclusive. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. - /// - /// An enumeration of summaries for the requested messages. - /// The minimum index. - /// The maximum index, or -1 to specify no upper bound. - /// The message summary items to fetch. - /// THe desired header fields. - /// The cancellation token. - Task> FetchAsync (int min, int max, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Fetch the message summaries for the messages between the two indexes, inclusive. - /// - /// - /// Fetches the message summaries for the messages between the two - /// indexes, inclusive. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. - /// - /// An enumeration of summaries for the requested messages. - /// The minimum index. - /// The maximum index, or -1 to specify no upper bound. - /// The message summary items to fetch. - /// THe desired header fields. - /// The cancellation token. - IList Fetch (int min, int max, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously fetch the message summaries for the messages between the two indexes, inclusive. - /// - /// - /// Asynchronously fetches the message summaries for the messages between - /// the two indexes, inclusive. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. - /// - /// An enumeration of summaries for the requested messages. - /// The minimum index. - /// The maximum index, or -1 to specify no upper bound. - /// The message summary items to fetch. - /// THe desired header fields. - /// The cancellation token. - Task> FetchAsync (int min, int max, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Fetch the message summaries for the messages between the two indexes (inclusive) - /// that have a higher mod-sequence value than the one specified. - /// - /// - /// Fetches the message summaries for the messages between the two - /// indexes (inclusive) that have a higher mod-sequence value than the one - /// specified. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. - /// - /// An enumeration of summaries for the requested messages. - /// The minimum index. - /// The maximum index, or -1 to specify no upper bound. - /// The mod-sequence value. - /// The message summary items to fetch. - /// The cancellation token. - IList Fetch (int min, int max, ulong modseq, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously fetch the message summaries for the messages between the two indexes - /// (inclusive) that have a higher mod-sequence value than the one specified. - /// - /// - /// Asynchronously fetches the message summaries for the messages between - /// the two indexes (inclusive) that have a higher mod-sequence value than the - /// one specified. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. - /// - /// An enumeration of summaries for the requested messages. - /// The minimum index. - /// The maximum index, or -1 to specify no upper bound. - /// The mod-sequence value. - /// The message summary items to fetch. - /// The cancellation token. - Task> FetchAsync (int min, int max, ulong modseq, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Fetch the message summaries for the messages between the two indexes (inclusive) - /// that have a higher mod-sequence value than the one specified. - /// - /// - /// Fetches the message summaries for the messages between the two - /// indexes (inclusive) that have a higher mod-sequence value than the one - /// specified. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. - /// - /// An enumeration of summaries for the requested messages. - /// The minimum index. - /// The maximum index, or -1 to specify no upper bound. - /// The mod-sequence value. - /// The message summary items to fetch. - /// The desired header fields. - /// The cancellation token. - IList Fetch (int min, int max, ulong modseq, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously fetch the message summaries for the messages between the two indexes - /// (inclusive) that have a higher mod-sequence value than the one specified. - /// - /// - /// Asynchronously fetches the message summaries for the messages between - /// the two indexes (inclusive) that have a higher mod-sequence value than the - /// one specified. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. - /// - /// An enumeration of summaries for the requested messages. - /// The minimum index. - /// The maximum index, or -1 to specify no upper bound. - /// The mod-sequence value. - /// The message summary items to fetch. - /// The desired header fields. - /// The cancellation token. - Task> FetchAsync (int min, int max, ulong modseq, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Fetch the message summaries for the messages between the two indexes (inclusive) - /// that have a higher mod-sequence value than the one specified. - /// - /// - /// Fetches the message summaries for the messages between the two - /// indexes (inclusive) that have a higher mod-sequence value than the one - /// specified. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. - /// - /// An enumeration of summaries for the requested messages. - /// The minimum index. - /// The maximum index, or -1 to specify no upper bound. - /// The mod-sequence value. - /// The message summary items to fetch. - /// The desired header fields. - /// The cancellation token. - IList Fetch (int min, int max, ulong modseq, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously fetch the message summaries for the messages between the two indexes - /// (inclusive) that have a higher mod-sequence value than the one specified. - /// - /// - /// Asynchronously fetches the message summaries for the messages between - /// the two indexes (inclusive) that have a higher mod-sequence value than the - /// one specified. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. - /// - /// An enumeration of summaries for the requested messages. - /// The minimum index. - /// The maximum index, or -1 to specify no upper bound. - /// The mod-sequence value. - /// The message summary items to fetch. - /// The desired header fields. - /// The cancellation token. - Task> FetchAsync (int min, int max, ulong modseq, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Get the specified message headers. - /// - /// - /// Gets the specified message headers. - /// - /// The message headers. - /// The UID of the message. - /// The cancellation token. - /// The progress reporting mechanism. - HeaderList GetHeaders (UniqueId uid, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Asynchronously get the specified message headers. - /// - /// - /// Asynchronously gets the specified message headers. - /// - /// The message headers. - /// The UID of the message. - /// The cancellation token. - /// The progress reporting mechanism. - Task GetHeadersAsync (UniqueId uid, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Get the specified body part headers. - /// - /// - /// Gets the specified body part headers. - /// - /// The body part headers. - /// The UID of the message. - /// The body part. - /// The cancellation token. - /// The progress reporting mechanism. - HeaderList GetHeaders (UniqueId uid, BodyPart part, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Asynchronously get the specified body part headers. - /// - /// - /// Asynchronously gets the specified body part headers. - /// - /// The body part headers. - /// The UID of the message. - /// The body part. - /// The cancellation token. - /// The progress reporting mechanism. - Task GetHeadersAsync (UniqueId uid, BodyPart part, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Get the specified message headers. - /// - /// - /// Gets the specified message headers. - /// - /// The message headers. - /// The index of the message. - /// The cancellation token. - /// The progress reporting mechanism. - HeaderList GetHeaders (int index, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Asynchronously get the specified message headers. - /// - /// - /// Asynchronously gets the specified message headers. - /// - /// The message headers. - /// The index of the message. - /// The cancellation token. - /// The progress reporting mechanism. - Task GetHeadersAsync (int index, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Get the specified body part headers. - /// - /// - /// Gets the specified body part headers. - /// - /// The body part headers. - /// The index of the message. - /// The body part. - /// The cancellation token. - /// The progress reporting mechanism. - HeaderList GetHeaders (int index, BodyPart part, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Asynchronously get the specified body part headers. - /// - /// - /// Asynchronously gets the specified body part headers. - /// - /// The body part headers. - /// The index of the message. - /// The body part. - /// The cancellation token. - /// The progress reporting mechanism. - Task GetHeadersAsync (int index, BodyPart part, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Get the specified message. - /// - /// - /// Gets the specified message. - /// - /// The message. - /// The UID of the message. - /// The cancellation token. - /// The progress reporting mechanism. - MimeMessage GetMessage (UniqueId uid, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Asynchronously get the specified message. - /// - /// - /// Asynchronously gets the specified message. - /// - /// The message. - /// The UID of the message. - /// The cancellation token. - /// The progress reporting mechanism. - Task GetMessageAsync (UniqueId uid, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Get the specified message. - /// - /// - /// Gets the specified message. - /// - /// The message. - /// The index of the message. - /// The cancellation token. - /// The progress reporting mechanism. - MimeMessage GetMessage (int index, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Asynchronously get the specified message. - /// - /// - /// Asynchronously gets the specified message. - /// - /// The message. - /// The index of the message. - /// The cancellation token. - /// The progress reporting mechanism. - Task GetMessageAsync (int index, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Get the specified body part. - /// - /// - /// Gets the specified body part. - /// - /// - /// - /// - /// The body part. - /// The UID of the message. - /// The body part. - /// The cancellation token. - /// The progress reporting mechanism. - MimeEntity GetBodyPart (UniqueId uid, BodyPart part, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Asynchronously get the specified body part. - /// - /// - /// Asynchronously gets the specified body part. - /// - /// The body part. - /// The UID of the message. - /// The body part. - /// The cancellation token. - /// The progress reporting mechanism. - Task GetBodyPartAsync (UniqueId uid, BodyPart part, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Get the specified body part. - /// - /// - /// Gets the specified body part. - /// - /// The body part. - /// The UID of the message. - /// The body part. - /// true if only the headers should be downloaded; otherwise, false> - /// The cancellation token. - /// The progress reporting mechanism. - MimeEntity GetBodyPart (UniqueId uid, BodyPart part, bool headersOnly, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Asynchronously get the specified body part. - /// - /// - /// Asynchronously gets the specified body part. - /// - /// The body part. - /// The UID of the message. - /// The body part. - /// true if only the headers should be downloaded; otherwise, false> - /// The cancellation token. - /// The progress reporting mechanism. - Task GetBodyPartAsync (UniqueId uid, BodyPart part, bool headersOnly, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Get the specified body part. - /// - /// - /// Gets the specified body part. - /// - /// The body part. - /// The index of the message. - /// The body part. - /// The cancellation token. - /// The progress reporting mechanism. - MimeEntity GetBodyPart (int index, BodyPart part, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Asynchronously get the specified body part. - /// - /// - /// Asynchronously gets the specified body part. - /// - /// The body part. - /// The index of the message. - /// The body part. - /// The cancellation token. - /// The progress reporting mechanism. - Task GetBodyPartAsync (int index, BodyPart part, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Get the specified body part. - /// - /// - /// Gets the specified body part. - /// - /// The body part. - /// The index of the message. - /// The body part. - /// true if only the headers should be downloaded; otherwise, false> - /// The cancellation token. - /// The progress reporting mechanism. - MimeEntity GetBodyPart (int index, BodyPart part, bool headersOnly, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Asynchronously get the specified body part. - /// - /// - /// Asynchronously gets the specified body part. - /// - /// The body part. - /// The index of the message. - /// The body part. - /// true if only the headers should be downloaded; otherwise, false> - /// The cancellation token. - /// The progress reporting mechanism. - Task GetBodyPartAsync (int index, BodyPart part, bool headersOnly, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Get a substream of the specified message. - /// - /// - /// Gets a substream of the message. If the starting offset is beyond - /// the end of the message, an empty stream is returned. If the number of - /// bytes desired extends beyond the end of the message, a truncated stream - /// will be returned. - /// - /// The stream. - /// The UID of the message. - /// The starting offset of the first desired byte. - /// The number of bytes desired. - /// The cancellation token. - /// The progress reporting mechanism. - Stream GetStream (UniqueId uid, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Asynchronously get a substream of the specified message. - /// - /// - /// Asynchronously gets a substream of the message. If the starting offset is beyond - /// the end of the message, an empty stream is returned. If the number of - /// bytes desired extends beyond the end of the message, a truncated stream - /// will be returned. - /// - /// The stream. - /// The UID of the message. - /// The starting offset of the first desired byte. - /// The number of bytes desired. - /// The cancellation token. - /// The progress reporting mechanism. - Task GetStreamAsync (UniqueId uid, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Get a substream of the specified message. - /// - /// - /// Gets a substream of the message. If the starting offset is beyond - /// the end of the message, an empty stream is returned. If the number of - /// bytes desired extends beyond the end of the message, a truncated stream - /// will be returned. - /// - /// The stream. - /// The index of the message. - /// The starting offset of the first desired byte. - /// The number of bytes desired. - /// The cancellation token. - /// The progress reporting mechanism. - Stream GetStream (int index, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Asynchronously get a substream of the specified message. - /// - /// - /// Asynchronously gets a substream of the message. If the starting offset is beyond - /// the end of the message, an empty stream is returned. If the number of - /// bytes desired extends beyond the end of the message, a truncated stream - /// will be returned. - /// - /// The stream. - /// The index of the message. - /// The starting offset of the first desired byte. - /// The number of bytes desired. - /// The cancellation token. - /// The progress reporting mechanism. - Task GetStreamAsync (int index, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Get a substream of the specified body part. - /// - /// - /// Gets a substream of the body part. If the starting offset is beyond - /// the end of the body part, an empty stream is returned. If the number of - /// bytes desired extends beyond the end of the body part, a truncated stream - /// will be returned. - /// - /// The stream. - /// The UID of the message. - /// The desired body part. - /// The starting offset of the first desired byte. - /// The number of bytes desired. - /// The cancellation token. - /// The progress reporting mechanism. - Stream GetStream (UniqueId uid, BodyPart part, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Asynchronously get a substream of the specified body part. - /// - /// - /// Asynchronously gets a substream of the body part. If the starting offset is beyond - /// the end of the body part, an empty stream is returned. If the number of - /// bytes desired extends beyond the end of the body part, a truncated stream - /// will be returned. - /// - /// The stream. - /// The UID of the message. - /// The desired body part. - /// The starting offset of the first desired byte. - /// The number of bytes desired. - /// The cancellation token. - /// The progress reporting mechanism. - Task GetStreamAsync (UniqueId uid, BodyPart part, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Get a substream of the specified body part. - /// - /// - /// Gets a substream of the body part. If the starting offset is beyond - /// the end of the body part, an empty stream is returned. If the number of - /// bytes desired extends beyond the end of the body part, a truncated stream - /// will be returned. - /// - /// The stream. - /// The index of the message. - /// The desired body part. - /// The starting offset of the first desired byte. - /// The number of bytes desired. - /// The cancellation token. - /// The progress reporting mechanism. - Stream GetStream (int index, BodyPart part, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Asynchronously get a substream of the specified body part. - /// - /// - /// Asynchronously gets a substream of the body part. If the starting offset is beyond - /// the end of the body part, an empty stream is returned. If the number of - /// bytes desired extends beyond the end of the body part, a truncated stream - /// will be returned. - /// - /// The stream. - /// The index of the message. - /// The desired body part. - /// The starting offset of the first desired byte. - /// The number of bytes desired. - /// The cancellation token. - /// The progress reporting mechanism. - Task GetStreamAsync (int index, BodyPart part, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Get a substream of the specified message. - /// - /// - /// Gets a substream of the specified message. - /// For more information about how to construct the , - /// see Section 6.4.5 of RFC3501. - /// - /// The stream. - /// The UID of the message. - /// The desired section of the message. - /// The cancellation token. - /// The progress reporting mechanism. - Stream GetStream (UniqueId uid, string section, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Asynchronously get a substream of the specified message. - /// - /// - /// Asynchronously gets a substream of the specified message. - /// For more information about how to construct the , - /// see Section 6.4.5 of RFC3501. - /// - /// The stream. - /// The UID of the message. - /// The desired section of the message. - /// The cancellation token. - /// The progress reporting mechanism. - Task GetStreamAsync (UniqueId uid, string section, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Get a substream of the specified message. - /// - /// - /// Gets a substream of the specified message. If the starting offset is beyond - /// the end of the specified section of the message, an empty stream is returned. If - /// the number of bytes desired extends beyond the end of the section, a truncated - /// stream will be returned. - /// For more information about how to construct the , - /// see Section 6.4.5 of RFC3501. - /// - /// The stream. - /// The UID of the message. - /// The desired section of the message. - /// The starting offset of the first desired byte. - /// The number of bytes desired. - /// The cancellation token. - /// The progress reporting mechanism. - Stream GetStream (UniqueId uid, string section, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Asynchronously get a substream of the specified message. - /// - /// - /// Asynchronously gets a substream of the specified message. If the starting - /// offset is beyond the end of the specified section of the message, an empty stream - /// is returned. If the number of bytes desired extends beyond the end of the section, - /// a truncated stream will be returned. - /// For more information about how to construct the , - /// see Section 6.4.5 of RFC3501. - /// - /// The stream. - /// The UID of the message. - /// The desired section of the message. - /// The starting offset of the first desired byte. - /// The number of bytes desired. - /// The cancellation token. - /// The progress reporting mechanism. - Task GetStreamAsync (UniqueId uid, string section, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Get a substream of the specified message. - /// - /// - /// Gets a substream of the specified message. - /// For more information about how to construct the , - /// see Section 6.4.5 of RFC3501. - /// - /// The stream. - /// The index of the message. - /// The desired section of the message. - /// The cancellation token. - /// The progress reporting mechanism. - Stream GetStream (int index, string section, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Asynchronously get a substream of the specified message. - /// - /// - /// Asynchronously gets a substream of the specified message. - /// For more information about how to construct the , - /// see Section 6.4.5 of RFC3501. - /// - /// The stream. - /// The index of the message. - /// The desired section of the message. - /// The cancellation token. - /// The progress reporting mechanism. - Task GetStreamAsync (int index, string section, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Get a substream of the specified message. - /// - /// - /// Gets a substream of the specified message. If the starting offset is beyond - /// the end of the specified section of the message, an empty stream is returned. If - /// the number of bytes desired extends beyond the end of the section, a truncated - /// stream will be returned. - /// For more information about how to construct the , - /// see Section 6.4.5 of RFC3501. - /// - /// The stream. - /// The index of the message. - /// The desired section of the message. - /// The starting offset of the first desired byte. - /// The number of bytes desired. - /// The cancellation token. - /// The progress reporting mechanism. - Stream GetStream (int index, string section, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Asynchronously get a substream of the specified message. - /// - /// - /// Asynchronously gets a substream of the specified message. If the starting - /// offset is beyond the end of the specified section of the message, an empty stream - /// is returned. If the number of bytes desired extends beyond the end of the section, - /// a truncated stream will be returned. - /// For more information about how to construct the , - /// see Section 6.4.5 of RFC3501. - /// - /// The stream. - /// The index of the message. - /// The desired section of the message. - /// The starting offset of the first desired byte. - /// The number of bytes desired. - /// The cancellation token. - /// The progress reporting mechanism. - Task GetStreamAsync (int index, string section, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Add a set of flags to the specified message. - /// - /// - /// Adds a set of flags to the specified message. - /// - /// The UID of the message. - /// The message flags to add. - /// If set to true, no events will be emitted. - /// The cancellation token. - void AddFlags (UniqueId uid, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously add a set of flags to the specified message. - /// - /// - /// Asynchronously adds a set of flags to the specified message. - /// - /// An asynchronous task context. - /// The UIDs of the message. - /// The message flags to add. - /// If set to true, no events will be emitted. - /// The cancellation token. - Task AddFlagsAsync (UniqueId uid, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Add a set of flags to the specified message. - /// - /// - /// Adds a set of flags to the specified message. - /// - /// The UID of the message. - /// The message flags to add. - /// A set of user-defined flags to add. - /// If set to true, no events will be emitted. - /// The cancellation token. - void AddFlags (UniqueId uid, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously add a set of flags to the specified message. - /// - /// - /// Asynchronously adds a set of flags to the specified message. - /// - /// An asynchronous task context. - /// The UIDs of the message. - /// The message flags to add. - /// A set of user-defined flags to add. - /// If set to true, no events will be emitted. - /// The cancellation token. - Task AddFlagsAsync (UniqueId uid, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Add a set of flags to the specified messages. - /// - /// - /// Adds a set of flags to the specified messages. - /// - /// The UIDs of the messages. - /// The message flags to add. - /// If set to true, no events will be emitted. - /// The cancellation token. - void AddFlags (IList uids, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously add a set of flags to the specified messages. - /// - /// - /// Asynchronously adds a set of flags to the specified messages. - /// - /// An asynchronous task context. - /// The UIDs of the messages. - /// The message flags to add. - /// If set to true, no events will be emitted. - /// The cancellation token. - Task AddFlagsAsync (IList uids, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Add a set of flags to the specified messages. - /// - /// - /// Adds a set of flags to the specified messages. - /// - /// The UIDs of the messages. - /// The message flags to add. - /// A set of user-defined flags to add. - /// If set to true, no events will be emitted. - /// The cancellation token. - void AddFlags (IList uids, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously add a set of flags to the specified messages. - /// - /// - /// Asynchronously adds a set of flags to the specified messages. - /// - /// An asynchronous task context. - /// The UIDs of the messages. - /// The message flags to add. - /// A set of user-defined flags to add. - /// If set to true, no events will be emitted. - /// The cancellation token. - Task AddFlagsAsync (IList uids, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Remove a set of flags from the specified message. - /// - /// - /// Removes a set of flags from the specified message. - /// - /// The UID of the message. - /// The message flags to remove. - /// If set to true, no events will be emitted. - /// The cancellation token. - void RemoveFlags (UniqueId uid, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously remove a set of flags from the specified message. - /// - /// - /// Asynchronously removes a set of flags from the specified message. - /// - /// An asynchronous task context. - /// The UID of the message. - /// The message flags to remove. - /// If set to true, no events will be emitted. - /// The cancellation token. - Task RemoveFlagsAsync (UniqueId uid, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Remove a set of flags from the specified message. - /// - /// - /// Removes a set of flags from the specified message. - /// - /// The UID of the message. - /// The message flags to remove. - /// A set of user-defined flags to remove. - /// If set to true, no events will be emitted. - /// The cancellation token. - void RemoveFlags (UniqueId uid, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously remove a set of flags from the specified message. - /// - /// - /// Asynchronously removes a set of flags from the specified message. - /// - /// An asynchronous task context. - /// The UID of the message. - /// The message flags to remove. - /// A set of user-defined flags to remove. - /// If set to true, no events will be emitted. - /// The cancellation token. - Task RemoveFlagsAsync (UniqueId uid, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Remove a set of flags from the specified messages. - /// - /// - /// Removes a set of flags from the specified messages. - /// - /// The UIDs of the messages. - /// The message flags to remove. - /// If set to true, no events will be emitted. - /// The cancellation token. - void RemoveFlags (IList uids, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously remove a set of flags from the specified messages. - /// - /// - /// Asynchronously removes a set of flags from the specified messages. - /// - /// An asynchronous task context. - /// The UIDs of the messages. - /// The message flags to remove. - /// If set to true, no events will be emitted. - /// The cancellation token. - Task RemoveFlagsAsync (IList uids, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Remove a set of flags from the specified messages. - /// - /// - /// Removes a set of flags from the specified messages. - /// - /// The UIDs of the messages. - /// The message flags to remove. - /// A set of user-defined flags to remove. - /// If set to true, no events will be emitted. - /// The cancellation token. - void RemoveFlags (IList uids, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously remove a set of flags from the specified messages. - /// - /// - /// Asynchronously removes a set of flags from the specified messages. - /// - /// An asynchronous task context. - /// The UIDs of the messages. - /// The message flags to remove. - /// A set of user-defined flags to remove. - /// If set to true, no events will be emitted. - /// The cancellation token. - Task RemoveFlagsAsync (IList uids, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Set the flags of the specified message. - /// - /// - /// Sets the flags of the specified message. - /// - /// The UID of the message. - /// The message flags to set. - /// If set to true, no events will be emitted. - /// The cancellation token. - void SetFlags (UniqueId uid, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously set the flags of the specified message. - /// - /// - /// Asynchronously sets the flags of the specified message. - /// - /// An asynchronous task context. - /// The UID of the message. - /// The message flags to set. - /// If set to true, no events will be emitted. - /// The cancellation token. - Task SetFlagsAsync (UniqueId uid, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Set the flags of the specified message. - /// - /// - /// Sets the flags of the specified message. - /// - /// The UID of the message. - /// The message flags to set. - /// A set of user-defined flags to set. - /// If set to true, no events will be emitted. - /// The cancellation token. - void SetFlags (UniqueId uid, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously set the flags of the specified message. - /// - /// - /// Asynchronously sets the flags of the specified message. - /// - /// An asynchronous task context. + /// The body part headers. /// The UID of the message. - /// The message flags to set. - /// A set of user-defined flags to set. - /// If set to true, no events will be emitted. - /// The cancellation token. - Task SetFlagsAsync (UniqueId uid, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Set the flags of the specified messages. - /// - /// - /// Sets the flags of the specified messages. - /// - /// The UIDs of the messages. - /// The message flags to set. - /// If set to true, no events will be emitted. - /// The cancellation token. - void SetFlags (IList uids, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously set the flags of the specified messages. - /// - /// - /// Asynchronously sets the flags of the specified messages. - /// - /// An asynchronous task context. - /// The UIDs of the messages. - /// The message flags to set. - /// If set to true, no events will be emitted. - /// The cancellation token. - Task SetFlagsAsync (IList uids, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Set the flags of the specified messages. - /// - /// - /// Sets the flags of the specified messages. - /// - /// The UIDs of the messages. - /// The message flags to set. - /// A set of user-defined flags to set. - /// If set to true, no events will be emitted. - /// The cancellation token. - void SetFlags (IList uids, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously set the flags of the specified messages. - /// - /// - /// Asynchronously sets the flags of the specified messages. - /// - /// An asynchronous task context. - /// The UIDs of the messages. - /// The message flags to set. - /// A set of user-defined flags to set. - /// If set to true, no events will be emitted. - /// The cancellation token. - Task SetFlagsAsync (IList uids, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Add a set of flags to the specified messages only if their mod-sequence value is less than the specified value. - /// - /// - /// Adds a set of flags to the specified messages only if their mod-sequence value is less than the specified value. - /// - /// The unique IDs of the messages that were not updated. - /// The UIDs of the messages. - /// The mod-sequence value. - /// The message flags to add. - /// If set to true, no events will be emitted. - /// The cancellation token. - IList AddFlags (IList uids, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously add a set of flags to the specified messages only if their mod-sequence value is less than the specified value. - /// - /// - /// Asynchronously adds a set of flags to the specified messages only if their mod-sequence value is less than the specified value. - /// - /// The unique IDs of the messages that were not updated. - /// The UIDs of the messages. - /// The mod-sequence value. - /// The message flags to add. - /// If set to true, no events will be emitted. - /// The cancellation token. - Task> AddFlagsAsync (IList uids, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Add a set of flags to the specified messages only if their mod-sequence value is less than the specified value. - /// - /// - /// Adds a set of flags to the specified messages only if their mod-sequence value is less than the specified value. - /// - /// The unique IDs of the messages that were not updated. - /// The UIDs of the messages. - /// The mod-sequence value. - /// The message flags to add. - /// A set of user-defined flags to set. - /// If set to true, no events will be emitted. - /// The cancellation token. - IList AddFlags (IList uids, ulong modseq, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously add a set of flags to the specified messages only if their mod-sequence value is less than the specified value. - /// - /// - /// Asynchronously adds a set of flags to the specified messages only if their mod-sequence value is less than the specified value. - /// - /// The unique IDs of the messages that were not updated. - /// The UIDs of the messages. - /// The mod-sequence value. - /// The message flags to add. - /// A set of user-defined flags to set. - /// If set to true, no events will be emitted. - /// The cancellation token. - Task> AddFlagsAsync (IList uids, ulong modseq, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Remove a set of flags from the specified messages only if their mod-sequence value is less than the specified value. - /// - /// - /// Removes a set of flags from the specified messages only if their mod-sequence value is less than the specified value. - /// - /// The unique IDs of the messages that were not updated. - /// The UIDs of the messages. - /// The mod-sequence value. - /// The message flags to remove. - /// If set to true, no events will be emitted. - /// The cancellation token. - IList RemoveFlags (IList uids, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously remove a set of flags from the specified messages only if their mod-sequence value is less than the specified value. - /// - /// - /// Asynchronously removes a set of flags from the specified messages only if their mod-sequence value is less than the specified value. - /// - /// The unique IDs of the messages that were not updated. - /// The UIDs of the messages. - /// The mod-sequence value. - /// The message flags to remove. - /// If set to true, no events will be emitted. - /// The cancellation token. - Task> RemoveFlagsAsync (IList uids, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Remove a set of flags from the specified messages only if their mod-sequence value is less than the specified value. - /// - /// - /// Removes a set of flags from the specified messages only if their mod-sequence value is less than the specified value. - /// - /// The unique IDs of the messages that were not updated. - /// The UIDs of the messages. - /// The mod-sequence value. - /// The message flags to remove. - /// A set of user-defined flags to remove. - /// If set to true, no events will be emitted. - /// The cancellation token. - IList RemoveFlags (IList uids, ulong modseq, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously remove a set of flags from the specified messages only if their mod-sequence value is less than the specified value. - /// - /// - /// Asynchronously removes a set of flags from the specified messages only if their mod-sequence value is less than the specified value. - /// - /// The unique IDs of the messages that were not updated. - /// The UIDs of the messages. - /// The mod-sequence value. - /// The message flags to remove. - /// A set of user-defined flags to remove. - /// If set to true, no events will be emitted. - /// The cancellation token. - Task> RemoveFlagsAsync (IList uids, ulong modseq, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Set the flags of the specified messages only if their mod-sequence value is less than the specified value. - /// - /// - /// Sets the flags of the specified messages only if their mod-sequence value is less than the specified value. - /// - /// The unique IDs of the messages that were not updated. - /// The UIDs of the messages. - /// The mod-sequence value. - /// The message flags to set. - /// If set to true, no events will be emitted. - /// The cancellation token. - IList SetFlags (IList uids, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously set the flags of the specified messages only if their mod-sequence value is less than the specified value. - /// - /// - /// Asynchronously sets the flags of the specified messages only if their mod-sequence value is less than the specified value. - /// - /// The unique IDs of the messages that were not updated. - /// The UIDs of the messages. - /// The mod-sequence value. - /// The message flags to set. - /// If set to true, no events will be emitted. - /// The cancellation token. - Task> SetFlagsAsync (IList uids, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Set the flags of the specified messages only if their mod-sequence value is less than the specified value. - /// - /// - /// Sets the flags of the specified messages only if their mod-sequence value is less than the specified value. - /// - /// The unique IDs of the messages that were not updated. - /// The UIDs of the messages. - /// The mod-sequence value. - /// The message flags to set. - /// A set of user-defined flags to set. - /// If set to true, no events will be emitted. - /// The cancellation token. - IList SetFlags (IList uids, ulong modseq, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously set the flags of the specified messages only if their mod-sequence value is less than the specified value. - /// - /// - /// Asynchronously sets the flags of the specified messages only if their mod-sequence value is less than the specified value. - /// - /// The unique IDs of the messages that were not updated. - /// The UIDs of the messages. - /// The mod-sequence value. - /// The message flags to set. - /// A set of user-defined flags to set. - /// If set to true, no events will be emitted. - /// The cancellation token. - Task> SetFlagsAsync (IList uids, ulong modseq, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Add a set of flags to the specified message. - /// - /// - /// Adds a set of flags to the specified message. - /// - /// The index of the message. - /// The message flags to add. - /// If set to true, no events will be emitted. - /// The cancellation token. - void AddFlags (int index, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously add a set of flags to the specified message. - /// - /// - /// Asynchronously adds a set of flags to the specified message. - /// - /// An asynchronous task context. - /// The index of the message. - /// The message flags to add. - /// If set to true, no events will be emitted. - /// The cancellation token. - Task AddFlagsAsync (int index, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Add a set of flags to the specified message. - /// - /// - /// Adds a set of flags to the specified message. - /// - /// The index of the message. - /// The message flags to add. - /// A set of user-defined flags to add. - /// If set to true, no events will be emitted. - /// The cancellation token. - void AddFlags (int index, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously add a set of flags to the specified message. - /// - /// - /// Asynchronously adds a set of flags to the specified message. - /// - /// An asynchronous task context. - /// The index of the message. - /// The message flags to add. - /// A set of user-defined flags to add. - /// If set to true, no events will be emitted. - /// The cancellation token. - Task AddFlagsAsync (int index, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Add a set of flags to the specified messages. - /// - /// - /// Adds a set of flags to the specified messages. - /// - /// The indexes of the messages. - /// The message flags to add. - /// If set to true, no events will be emitted. - /// The cancellation token. - void AddFlags (IList indexes, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously add a set of flags to the specified messages. - /// - /// - /// Asynchronously adds a set of flags to the specified messages. - /// - /// An asynchronous task context. - /// The indexes of the messages. - /// The message flags to add. - /// If set to true, no events will be emitted. - /// The cancellation token. - Task AddFlagsAsync (IList indexes, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Add a set of flags to the specified messages. - /// - /// - /// Adds a set of flags to the specified messages. - /// - /// The indexes of the messages. - /// The message flags to add. - /// A set of user-defined flags to add. - /// If set to true, no events will be emitted. + /// The body part. /// The cancellation token. - void AddFlags (IList indexes, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + /// The progress reporting mechanism. + /// + /// is . + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested body part headers. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + HeaderList GetHeaders (UniqueId uid, BodyPart part, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Asynchronously add a set of flags to the specified messages. + /// Asynchronously get the specified body part headers. /// /// - /// Asynchronously adds a set of flags to the specified messages. + /// Asynchronously gets the specified body part headers. /// - /// An asynchronous task context. - /// The indexes of the messages. - /// The message flags to add. - /// A set of user-defined flags to add. - /// If set to true, no events will be emitted. + /// The body part headers. + /// The UID of the message. + /// The body part. /// The cancellation token. - Task AddFlagsAsync (IList indexes, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + /// The progress reporting mechanism. + /// + /// is . + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested body part headers. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task GetHeadersAsync (UniqueId uid, BodyPart part, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Remove a set of flags from the specified message. + /// Get the specified message headers. /// /// - /// Removes a set of flags from the specified message. + /// Gets the specified message headers. /// + /// The message headers. /// The index of the message. - /// The message flags to remove. - /// If set to true, no events will be emitted. /// The cancellation token. - void RemoveFlags (int index, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + /// The progress reporting mechanism. + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message headers. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + HeaderList GetHeaders (int index, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Asynchronously remove a set of flags from the specified message. + /// Asynchronously get the specified message headers. /// /// - /// Asynchronously removes a set of flags from the specified message. + /// Asynchronously gets the specified message headers. /// - /// An asynchronous task context. + /// The message headers. /// The index of the message. - /// The message flags to remove. - /// If set to true, no events will be emitted. /// The cancellation token. - Task RemoveFlagsAsync (int index, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + /// The progress reporting mechanism. + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message headers. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task GetHeadersAsync (int index, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Remove a set of flags from the specified message. + /// Get the specified body part headers. /// /// - /// Removes a set of flags from the specified message. + /// Gets the specified body part headers. /// + /// The body part headers. /// The index of the message. - /// The message flags to remove. - /// A set of user-defined flags to remove. - /// If set to true, no events will be emitted. + /// The body part. /// The cancellation token. - void RemoveFlags (int index, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + /// The progress reporting mechanism. + /// + /// is out of range. + /// + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested body part headers. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + HeaderList GetHeaders (int index, BodyPart part, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Asynchronously remove a set of flags from the specified message. + /// Asynchronously get the specified body part headers. /// /// - /// Asynchronously removes a set of flags from the specified message. + /// Asynchronously gets the specified body part headers. /// - /// An asynchronous task context. + /// The body part headers. /// The index of the message. - /// The message flags to remove. - /// A set of user-defined flags to remove. - /// If set to true, no events will be emitted. - /// The cancellation token. - Task RemoveFlagsAsync (int index, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Remove a set of flags from the specified messages. - /// - /// - /// Removes a set of flags from the specified messages. - /// - /// The indexes of the messages. - /// The message flags to remove. - /// If set to true, no events will be emitted. - /// The cancellation token. - void RemoveFlags (IList indexes, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously remove a set of flags from the specified messages. - /// - /// - /// Asynchronously removes a set of flags from the specified messages. - /// - /// An asynchronous task context. - /// The indexes of the messages. - /// The message flags to remove. - /// If set to true, no events will be emitted. - /// The cancellation token. - Task RemoveFlagsAsync (IList indexes, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Remove a set of flags from the specified messages. - /// - /// - /// Removes a set of flags from the specified messages. - /// - /// The indexes of the messages. - /// The message flags to remove. - /// A set of user-defined flags to remove. - /// If set to true, no events will be emitted. - /// The cancellation token. - void RemoveFlags (IList indexes, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously remove a set of flags from the specified messages. - /// - /// - /// Asynchronously removes a set of flags from the specified messages. - /// - /// An asynchronous task context. - /// The indexes of the messages. - /// The message flags to remove. - /// A set of user-defined flags to remove. - /// If set to true, no events will be emitted. + /// The body part. /// The cancellation token. - Task RemoveFlagsAsync (IList indexes, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + /// The progress reporting mechanism. + /// + /// is out of range. + /// + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested body part headers. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task GetHeadersAsync (int index, BodyPart part, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Set the flags of the specified message. + /// Get the specified message. /// /// - /// Sets the flags of the specified message. + /// Gets the specified message. /// - /// The index of the message. - /// The message flags to set. - /// If set to true, no events will be emitted. + /// + /// + /// + /// The message. + /// The UID of the message. /// The cancellation token. - void SetFlags (int index, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + /// The progress reporting mechanism. + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + MimeMessage GetMessage (UniqueId uid, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Asynchronously set the flags of the specified message. + /// Asynchronously get the specified message. /// /// - /// Asynchronously sets the flags of the specified message. + /// Asynchronously gets the specified message. /// - /// An asynchronous task context. - /// The index of the message. - /// The message flags to set. - /// If set to true, no events will be emitted. + /// + /// + /// + /// The message. + /// The UID of the message. /// The cancellation token. - Task SetFlagsAsync (int index, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + /// The progress reporting mechanism. + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task GetMessageAsync (UniqueId uid, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Set the flags of the specified message. + /// Get the specified message. /// /// - /// Sets the flags of the specified message. + /// Gets the specified message. /// + /// + /// + /// + /// The message. /// The index of the message. - /// The message flags to set. - /// A set of user-defined flags to set. - /// If set to true, no events will be emitted. /// The cancellation token. - void SetFlags (int index, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + /// The progress reporting mechanism. + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + MimeMessage GetMessage (int index, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Asynchronously set the flags of the specified message. + /// Asynchronously get the specified message. /// /// - /// Asynchronously sets the flags of the specified message. + /// Asynchronously gets the specified message. /// - /// An asynchronous task context. + /// + /// + /// + /// The message. /// The index of the message. - /// The message flags to set. - /// A set of user-defined flags to set. - /// If set to true, no events will be emitted. - /// The cancellation token. - Task SetFlagsAsync (int index, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Set the flags of the specified messages. - /// - /// - /// Sets the flags of the specified messages. - /// - /// The indexes of the messages. - /// The message flags to set. - /// If set to true, no events will be emitted. - /// The cancellation token. - void SetFlags (IList indexes, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously set the flags of the specified messages. - /// - /// - /// Asynchronously sets the flags of the specified messages. - /// - /// An asynchronous task context. - /// The indexes of the messages. - /// The message flags to set. - /// If set to true, no events will be emitted. - /// The cancellation token. - Task SetFlagsAsync (IList indexes, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Set the flags of the specified messages. - /// - /// - /// Sets the flags of the specified messages. - /// - /// The indexes of the messages. - /// The message flags to set. - /// A set of user-defined flags to set. - /// If set to true, no events will be emitted. - /// The cancellation token. - void SetFlags (IList indexes, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously set the flags of the specified messages. - /// - /// - /// Asynchronously sets the flags of the specified messages. - /// - /// An asynchronous task context. - /// The indexes of the messages. - /// The message flags to set. - /// A set of user-defined flags to set. - /// If set to true, no events will be emitted. - /// The cancellation token. - Task SetFlagsAsync (IList indexes, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Add a set of flags to the specified messages only if their mod-sequence value is less than the specified value. - /// - /// - /// Adds a set of flags to the specified messages only if their mod-sequence value is less than the specified value. - /// - /// The indexes of the messages that were not updated. - /// The indexes of the messages. - /// The mod-sequence value. - /// The message flags to add. - /// If set to true, no events will be emitted. - /// The cancellation token. - IList AddFlags (IList indexes, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously add a set of flags to the specified messages only if their mod-sequence value is less than the specified value. - /// - /// - /// Asynchronously adds a set of flags to the specified messages only if their mod-sequence value is less than the specified value. - /// - /// The indexes of the messages that were not updated. - /// The indexes of the messages. - /// The mod-sequence value. - /// The message flags to add. - /// If set to true, no events will be emitted. - /// The cancellation token. - Task> AddFlagsAsync (IList indexes, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Add a set of flags to the specified messages only if their mod-sequence value is less than the specified value. - /// - /// - /// Adds a set of flags to the specified messages only if their mod-sequence value is less than the specified value. - /// - /// The indexes of the messages that were not updated. - /// The indexes of the messages. - /// The mod-sequence value. - /// The message flags to add. - /// A set of user-defined flags to add. - /// If set to true, no events will be emitted. - /// The cancellation token. - IList AddFlags (IList indexes, ulong modseq, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously add a set of flags to the specified messages only if their mod-sequence value is less than the specified value. - /// - /// - /// Asynchronously adds a set of flags to the specified messages only if their mod-sequence value is less than the specified value. - /// - /// The indexes of the messages that were not updated. - /// The indexes of the messages. - /// The mod-sequence value. - /// The message flags to add. - /// A set of user-defined flags to add. - /// If set to true, no events will be emitted. - /// The cancellation token. - Task> AddFlagsAsync (IList indexes, ulong modseq, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Remove a set of flags from the specified messages only if their mod-sequence value is less than the specified value. - /// - /// - /// Removes a set of flags from the specified messages only if their mod-sequence value is less than the specified value. - /// - /// The indexes of the messages that were not updated. - /// The indexes of the messages. - /// The mod-sequence value. - /// The message flags to remove. - /// If set to true, no events will be emitted. - /// The cancellation token. - IList RemoveFlags (IList indexes, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously remove a set of flags from the specified messages only if their mod-sequence value is less than the specified value. - /// - /// - /// Asynchronously removes a set of flags from the specified messages only if their mod-sequence value is less than the specified value. - /// - /// The indexes of the messages that were not updated. - /// The indexes of the messages. - /// The mod-sequence value. - /// The message flags to remove. - /// If set to true, no events will be emitted. - /// The cancellation token. - Task> RemoveFlagsAsync (IList indexes, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Remove a set of flags from the specified messages only if their mod-sequence value is less than the specified value. - /// - /// - /// Removes a set of flags from the specified messages only if their mod-sequence value is less than the specified value. - /// - /// The indexes of the messages that were not updated. - /// The indexes of the messages. - /// The mod-sequence value. - /// The message flags to remove. - /// A set of user-defined flags to remove. - /// If set to true, no events will be emitted. - /// The cancellation token. - IList RemoveFlags (IList indexes, ulong modseq, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously remove a set of flags from the specified messages only if their mod-sequence value is less than the specified value. - /// - /// - /// Asynchronously removes a set of flags from the specified messages only if their mod-sequence value is less than the specified value. - /// - /// The indexes of the messages that were not updated. - /// The indexes of the messages. - /// The mod-sequence value. - /// The message flags to remove. - /// A set of user-defined flags to remove. - /// If set to true, no events will be emitted. - /// The cancellation token. - Task> RemoveFlagsAsync (IList indexes, ulong modseq, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Set the flags of the specified messages only if their mod-sequence value is less than the specified value. - /// - /// - /// Sets the flags of the specified messages only if their mod-sequence value is less than the specified value. - /// - /// The indexes of the messages that were not updated. - /// The indexes of the messages. - /// The mod-sequence value. - /// The message flags to set. - /// If set to true, no events will be emitted. - /// The cancellation token. - IList SetFlags (IList indexes, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously set the flags of the specified messages only if their mod-sequence value is less than the specified value. - /// - /// - /// Asynchronously sets the flags of the specified messages only if their mod-sequence value is less than the specified value. - /// - /// The indexes of the messages that were not updated. - /// The indexes of the messages. - /// The mod-sequence value. - /// The message flags to set. - /// If set to true, no events will be emitted. - /// The cancellation token. - Task> SetFlagsAsync (IList indexes, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Set the flags of the specified messages only if their mod-sequence value is less than the specified value. - /// - /// - /// Sets the flags of the specified messages only if their mod-sequence value is less than the specified value. - /// - /// The indexes of the messages that were not updated. - /// The indexes of the messages. - /// The mod-sequence value. - /// The message flags to set. - /// A set of user-defined flags to set. - /// If set to true, no events will be emitted. - /// The cancellation token. - IList SetFlags (IList indexes, ulong modseq, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously set the flags of the specified messages only if their mod-sequence value is less than the specified value. - /// - /// - /// Asynchronously sets the flags of the specified messages only if their mod-sequence value is less than the specified value. - /// - /// The indexes of the messages that were not updated. - /// The indexes of the messages. - /// The mod-sequence value. - /// The message flags to set. - /// A set of user-defined flags to set. - /// If set to true, no events will be emitted. - /// The cancellation token. - Task> SetFlagsAsync (IList indexes, ulong modseq, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Add a set of labels to the specified message. - /// - /// - /// Adds a set of labels to the specified message. - /// - /// The UID of the message. - /// The labels to add. - /// If set to true, no events will be emitted. - /// The cancellation token. - void AddLabels (UniqueId uid, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously add a set of labels to the specified message. - /// - /// - /// Asynchronously adds a set of labels to the specified message. - /// - /// An asynchronous task context. - /// The UIDs of the message. - /// The labels to add. - /// If set to true, no events will be emitted. - /// The cancellation token. - Task AddLabelsAsync (UniqueId uid, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Add a set of labels to the specified messages. - /// - /// - /// Adds a set of labels to the specified messages. - /// - /// The UIDs of the messages. - /// The labels to add. - /// If set to true, no events will be emitted. - /// The cancellation token. - void AddLabels (IList uids, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously add a set of labels to the specified messages. - /// - /// - /// Asynchronously adds a set of labels to the specified messages. - /// - /// An asynchronous task context. - /// The UIDs of the messages. - /// The labels to add. - /// If set to true, no events will be emitted. /// The cancellation token. - Task AddLabelsAsync (IList uids, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + /// The progress reporting mechanism. + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task GetMessageAsync (int index, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Remove a set of labels from the specified message. + /// Get the specified body part. /// /// - /// Removes a set of labels from the specified message. + /// Gets the specified body part. /// + /// + /// + /// + /// The body part. /// The UID of the message. - /// The labels to remove. - /// If set to true, no events will be emitted. + /// The body part. /// The cancellation token. - void RemoveLabels (UniqueId uid, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + /// The progress reporting mechanism. + /// + /// is . + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message body. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + MimeEntity GetBodyPart (UniqueId uid, BodyPart part, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Asynchronously remove a set of labels from the specified message. + /// Asynchronously get the specified body part. /// /// - /// Asynchronously removes a set of labels from the specified message. + /// Asynchronously gets the specified body part. /// - /// An asynchronous task context. + /// + /// + /// + /// The body part. /// The UID of the message. - /// The labels to remove. - /// If set to true, no events will be emitted. + /// The body part. /// The cancellation token. - Task RemoveLabelsAsync (UniqueId uid, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + /// The progress reporting mechanism. + /// + /// is . + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message body. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task GetBodyPartAsync (UniqueId uid, BodyPart part, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Remove a set of labels from the specified messages. + /// Get the specified body part. /// /// - /// Removes a set of labels from the specified messages. + /// Gets the specified body part. /// - /// The UIDs of the messages. - /// The labels to remove. - /// If set to true, no events will be emitted. + /// The body part. + /// The index of the message. + /// The body part. /// The cancellation token. - void RemoveLabels (IList uids, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + /// The progress reporting mechanism. + /// + /// is . + /// + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message body. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + MimeEntity GetBodyPart (int index, BodyPart part, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Asynchronously remove a set of labels from the specified messages. + /// Asynchronously get the specified body part. /// /// - /// Asynchronously removes a set of labels from the specified messages. + /// Asynchronously gets the specified body part. /// - /// An asynchronous task context. - /// The UIDs of the messages. - /// The labels to remove. - /// If set to true, no events will be emitted. + /// The body part. + /// The index of the message. + /// The body part. /// The cancellation token. - Task RemoveLabelsAsync (IList uids, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Set the labels of the specified message. - /// - /// - /// Sets the labels of the specified message. + /// The progress reporting mechanism. + /// + /// is . + /// + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message body. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task GetBodyPartAsync (int index, BodyPart part, CancellationToken cancellationToken = default, ITransferProgress? progress = null); + + /// + /// Get a message stream. + /// + /// + /// Gets a message stream. /// + /// + /// + /// + /// The message stream. /// The UID of the message. - /// The labels to set. - /// If set to true, no events will be emitted. /// The cancellation token. - void SetLabels (UniqueId uid, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously set the labels of the specified message. - /// - /// - /// Asynchronously sets the labels of the specified message. + /// The progress reporting mechanism. + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Stream GetStream (UniqueId uid, CancellationToken cancellationToken = default, ITransferProgress? progress = null); + + /// + /// Asynchronously get a message stream. + /// + /// + /// Asynchronously gets a message stream. /// - /// An asynchronous task context. + /// + /// + /// + /// The message stream. /// The UID of the message. - /// The labels to set. - /// If set to true, no events will be emitted. /// The cancellation token. - Task SetLabelsAsync (UniqueId uid, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Set the labels of the specified messages. - /// - /// - /// Sets the labels of the specified messages. + /// The progress reporting mechanism. + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task GetStreamAsync (UniqueId uid, CancellationToken cancellationToken = default, ITransferProgress? progress = null); + + /// + /// Get a message stream. + /// + /// + /// Gets a message stream. /// - /// The UIDs of the messages. - /// The labels to set. - /// If set to true, no events will be emitted. + /// + /// + /// + /// The message stream. + /// The index of the message. /// The cancellation token. - void SetLabels (IList uids, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously set the labels of the specified messages. - /// - /// - /// Asynchronously sets the labels of the specified messages. + /// The progress reporting mechanism. + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Stream GetStream (int index, CancellationToken cancellationToken = default, ITransferProgress? progress = null); + + /// + /// Asynchronously get a message stream. + /// + /// + /// Asynchronously gets a message stream. /// - /// An asynchronous task context. - /// The UIDs of the messages. - /// The labels to set. - /// If set to true, no events will be emitted. + /// + /// + /// + /// The message stream. + /// The index of the message. /// The cancellation token. - Task SetLabelsAsync (IList uids, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + /// The progress reporting mechanism. + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task GetStreamAsync (int index, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Add a set of labels to the specified messages only if their mod-sequence value is less than the specified value. + /// Get a substream of the specified message. /// /// - /// Adds a set of labels to the specified messages only if their mod-sequence value is less than the specified value. + /// Gets a substream of the message. If the starting offset is beyond + /// the end of the message, an empty stream is returned. If the number of + /// bytes desired extends beyond the end of the message, a truncated stream + /// will be returned. /// - /// The unique IDs of the messages that were not updated. - /// The UIDs of the messages. - /// The mod-sequence value. - /// The labels to add. - /// If set to true, no events will be emitted. + /// The stream. + /// The UID of the message. + /// The starting offset of the first desired byte. + /// The number of bytes desired. /// The cancellation token. - IList AddLabels (IList uids, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + /// The progress reporting mechanism. + /// + /// is invalid. + /// + /// + /// is negative. + /// -or- + /// is negative. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Stream GetStream (UniqueId uid, int offset, int count, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Asynchronously add a set of labels to the specified messages only if their mod-sequence value is less than the specified value. + /// Asynchronously get a substream of the specified message. /// /// - /// Asynchronously adds a set of labels to the specified messages only if their mod-sequence value is less than the specified value. + /// Asynchronously gets a substream of the message. If the starting offset is beyond + /// the end of the message, an empty stream is returned. If the number of + /// bytes desired extends beyond the end of the message, a truncated stream + /// will be returned. /// - /// The unique IDs of the messages that were not updated. - /// The UIDs of the messages. - /// The mod-sequence value. - /// The labels to add. - /// If set to true, no events will be emitted. + /// The stream. + /// The UID of the message. + /// The starting offset of the first desired byte. + /// The number of bytes desired. /// The cancellation token. - Task> AddLabelsAsync (IList uids, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + /// The progress reporting mechanism. + /// + /// is invalid. + /// + /// + /// is negative. + /// -or- + /// is negative. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task GetStreamAsync (UniqueId uid, int offset, int count, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Remove a set of labels from the specified messages only if their mod-sequence value is less than the specified value. + /// Get a substream of the specified message. /// /// - /// Removes a set of labels from the specified messages only if their mod-sequence value is less than the specified value. + /// Gets a substream of the message. If the starting offset is beyond + /// the end of the message, an empty stream is returned. If the number of + /// bytes desired extends beyond the end of the message, a truncated stream + /// will be returned. /// - /// The unique IDs of the messages that were not updated. - /// The UIDs of the messages. - /// The mod-sequence value. - /// The labels to remove. - /// If set to true, no events will be emitted. + /// The stream. + /// The index of the message. + /// The starting offset of the first desired byte. + /// The number of bytes desired. /// The cancellation token. - IList RemoveLabels (IList uids, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + /// The progress reporting mechanism. + /// + /// is out of range. + /// -or- + /// is negative. + /// -or- + /// is negative. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Stream GetStream (int index, int offset, int count, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Asynchronously remove a set of labels from the specified messages only if their mod-sequence value is less than the specified value. + /// Asynchronously get a substream of the specified message. /// /// - /// Asynchronously removes a set of labels from the specified messages only if their mod-sequence value is less than the specified value. + /// Asynchronously gets a substream of the message. If the starting offset is beyond + /// the end of the message, an empty stream is returned. If the number of + /// bytes desired extends beyond the end of the message, a truncated stream + /// will be returned. /// - /// The unique IDs of the messages that were not updated. - /// The UIDs of the messages. - /// The mod-sequence value. - /// The labels to remove. - /// If set to true, no events will be emitted. + /// The stream. + /// The index of the message. + /// The starting offset of the first desired byte. + /// The number of bytes desired. /// The cancellation token. - Task> RemoveLabelsAsync (IList uids, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Set the labels of the specified messages only if their mod-sequence value is less than the specified value. - /// - /// - /// Sets the labels of the specified messages only if their mod-sequence value is less than the specified value. + /// The progress reporting mechanism. + /// + /// is out of range. + /// -or- + /// is negative. + /// -or- + /// is negative. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task GetStreamAsync (int index, int offset, int count, CancellationToken cancellationToken = default, ITransferProgress? progress = null); + + /// + /// Get a body part as a stream. + /// + /// + /// Gets a body part as a stream. /// - /// The unique IDs of the messages that were not updated. - /// The UIDs of the messages. - /// The mod-sequence value. - /// The labels to set. - /// If set to true, no events will be emitted. + /// + /// + /// + /// The body part stream. + /// The UID of the message. + /// The desired body part. /// The cancellation token. - IList SetLabels (IList uids, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously set the labels of the specified messages only if their mod-sequence value is less than the specified value. - /// - /// - /// Asynchronously sets the labels of the specified messages only if their mod-sequence value is less than the specified value. + /// The progress reporting mechanism. + /// + /// is invalid. + /// + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Stream GetStream (UniqueId uid, BodyPart part, CancellationToken cancellationToken = default, ITransferProgress? progress = null); + + /// + /// Asynchronously get a body part as a stream. + /// + /// + /// Asynchronously gets a body part as a stream. /// - /// The unique IDs of the messages that were not updated. - /// The UIDs of the messages. - /// The mod-sequence value. - /// The labels to set. - /// If set to true, no events will be emitted. + /// + /// + /// + /// The body part stream. + /// The UID of the message. + /// The desired body part. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is invalid. + /// + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task GetStreamAsync (UniqueId uid, BodyPart part, CancellationToken cancellationToken = default, ITransferProgress? progress = null); + + /// + /// Get a body part as a stream. + /// + /// + /// Gets a body part as a stream. + /// + /// The body part stream. + /// The index of the message. + /// The desired body part. /// The cancellation token. - Task> SetLabelsAsync (IList uids, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + /// The progress reporting mechanism. + /// + /// is . + /// + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Stream GetStream (int index, BodyPart part, CancellationToken cancellationToken = default, ITransferProgress? progress = null); + + /// + /// Asynchronously get a body part as a stream. + /// + /// + /// Asynchronously gets a body part as a stream. + /// + /// The body part stream. + /// The index of the message. + /// The desired body part. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task GetStreamAsync (int index, BodyPart part, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Add a set of labels to the specified message. + /// Get a substream of the specified body part. /// /// - /// Adds a set of labels to the specified message. + /// Gets a substream of the body part. If the starting offset is beyond + /// the end of the body part, an empty stream is returned. If the number of + /// bytes desired extends beyond the end of the body part, a truncated stream + /// will be returned. /// - /// The index of the message. - /// The labels to add. - /// If set to true, no events will be emitted. + /// The stream. + /// The UID of the message. + /// The desired body part. + /// The starting offset of the first desired byte. + /// The number of bytes desired. /// The cancellation token. - void AddLabels (int index, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + /// The progress reporting mechanism. + /// + /// is invalid. + /// + /// + /// is . + /// + /// + /// is negative. + /// -or- + /// is negative. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Stream GetStream (UniqueId uid, BodyPart part, int offset, int count, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Asynchronously add a set of labels to the specified message. + /// Asynchronously get a substream of the specified body part. /// /// - /// Asynchronously adds a set of labels to the specified message. + /// Asynchronously gets a substream of the body part. If the starting offset is beyond + /// the end of the body part, an empty stream is returned. If the number of + /// bytes desired extends beyond the end of the body part, a truncated stream + /// will be returned. /// - /// An asynchronous task context. - /// The index of the message. - /// The labels to add. - /// If set to true, no events will be emitted. + /// The stream. + /// The UID of the message. + /// The desired body part. + /// The starting offset of the first desired byte. + /// The number of bytes desired. /// The cancellation token. - Task AddLabelsAsync (int index, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + /// The progress reporting mechanism. + /// + /// is invalid. + /// + /// + /// is . + /// + /// + /// is negative. + /// -or- + /// is negative. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task GetStreamAsync (UniqueId uid, BodyPart part, int offset, int count, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Add a set of labels to the specified messages. + /// Get a substream of the specified body part. /// /// - /// Adds a set of labels to the specified messages. + /// Gets a substream of the body part. If the starting offset is beyond + /// the end of the body part, an empty stream is returned. If the number of + /// bytes desired extends beyond the end of the body part, a truncated stream + /// will be returned. /// - /// The indexes of the messages. - /// The labels to add. - /// If set to true, no events will be emitted. + /// The stream. + /// The index of the message. + /// The desired body part. + /// The starting offset of the first desired byte. + /// The number of bytes desired. /// The cancellation token. - void AddLabels (IList indexes, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + /// The progress reporting mechanism. + /// + /// is . + /// + /// + /// is out of range. + /// -or- + /// is negative. + /// -or- + /// is negative. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Stream GetStream (int index, BodyPart part, int offset, int count, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Asynchronously add a set of labels to the specified messages. + /// Asynchronously get a substream of the specified body part. /// /// - /// Asynchronously adds a set of labels to the specified messages. + /// Asynchronously gets a substream of the body part. If the starting offset is beyond + /// the end of the body part, an empty stream is returned. If the number of + /// bytes desired extends beyond the end of the body part, a truncated stream + /// will be returned. /// - /// An asynchronous task context. - /// The indexes of the messages. - /// The labels to add. - /// If set to true, no events will be emitted. + /// The stream. + /// The index of the message. + /// The desired body part. + /// The starting offset of the first desired byte. + /// The number of bytes desired. /// The cancellation token. - Task AddLabelsAsync (IList indexes, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + /// The progress reporting mechanism. + /// + /// is . + /// + /// + /// is out of range. + /// -or- + /// is negative. + /// -or- + /// is negative. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task GetStreamAsync (int index, BodyPart part, int offset, int count, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Remove a set of labels from the specified message. + /// Get a substream of the specified message. /// /// - /// Removes a set of labels from the specified message. + /// Gets a substream of the specified message. + /// For more information about how to construct the , + /// see Section 6.4.5 of RFC3501. /// - /// The index of the message. - /// The labels to remove. - /// If set to true, no events will be emitted. + /// + /// + /// + /// The stream. + /// The UID of the message. + /// The desired section of the message. /// The cancellation token. - void RemoveLabels (int index, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + /// The progress reporting mechanism. + /// + /// is invalid. + /// + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Stream GetStream (UniqueId uid, string section, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Asynchronously remove a set of labels from the specified message. + /// Asynchronously get a substream of the specified message. /// /// - /// Asynchronously removes a set of labels from the specified message. + /// Asynchronously gets a substream of the specified message. + /// For more information about how to construct the , + /// see Section 6.4.5 of RFC3501. /// - /// An asynchronous task context. - /// The index of the message. - /// The labels to remove. - /// If set to true, no events will be emitted. + /// + /// + /// + /// The stream. + /// The UID of the message. + /// The desired section of the message. /// The cancellation token. - Task RemoveLabelsAsync (int index, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + /// The progress reporting mechanism. + /// + /// is invalid. + /// + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task GetStreamAsync (UniqueId uid, string section, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Remove a set of labels from the specified messages. + /// Get a substream of the specified message. /// /// - /// Removes a set of labels from the specified messages. + /// Gets a substream of the specified message. If the starting offset is beyond + /// the end of the specified section of the message, an empty stream is returned. If + /// the number of bytes desired extends beyond the end of the section, a truncated + /// stream will be returned. + /// For more information about how to construct the , + /// see Section 6.4.5 of RFC3501. /// - /// The indexes of the messages. - /// The labels to remove. - /// If set to true, no events will be emitted. + /// The stream. + /// The UID of the message. + /// The desired section of the message. + /// The starting offset of the first desired byte. + /// The number of bytes desired. /// The cancellation token. - void RemoveLabels (IList indexes, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + /// The progress reporting mechanism. + /// + /// is invalid. + /// + /// + /// is . + /// + /// + /// is negative. + /// -or- + /// is negative. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Stream GetStream (UniqueId uid, string section, int offset, int count, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Asynchronously remove a set of labels from the specified messages. + /// Asynchronously get a substream of the specified message. /// /// - /// Asynchronously removes a set of labels from the specified messages. + /// Asynchronously gets a substream of the specified message. If the starting + /// offset is beyond the end of the specified section of the message, an empty stream + /// is returned. If the number of bytes desired extends beyond the end of the section, + /// a truncated stream will be returned. + /// For more information about how to construct the , + /// see Section 6.4.5 of RFC3501. /// - /// An asynchronous task context. - /// The indexes of the messages. - /// The labels to remove. - /// If set to true, no events will be emitted. + /// The stream. + /// The UID of the message. + /// The desired section of the message. + /// The starting offset of the first desired byte. + /// The number of bytes desired. /// The cancellation token. - Task RemoveLabelsAsync (IList indexes, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + /// The progress reporting mechanism. + /// + /// is invalid. + /// + /// + /// is . + /// + /// + /// is negative. + /// -or- + /// is negative. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task GetStreamAsync (UniqueId uid, string section, int offset, int count, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Set the labels of the specified message. + /// Get a substream of the specified message. /// /// - /// Sets the labels of the specified message. + /// Gets a substream of the specified message. + /// For more information about how to construct the , + /// see Section 6.4.5 of RFC3501. /// + /// The stream. /// The index of the message. - /// The labels to set. - /// If set to true, no events will be emitted. + /// The desired section of the message. /// The cancellation token. - void SetLabels (int index, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + /// The progress reporting mechanism. + /// + /// is . + /// + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Stream GetStream (int index, string section, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Asynchronously set the labels of the specified message. + /// Asynchronously get a substream of the specified message. /// /// - /// Asynchronously sets the labels of the specified message. + /// Asynchronously gets a substream of the specified message. + /// For more information about how to construct the , + /// see Section 6.4.5 of RFC3501. /// - /// An asynchronous task context. + /// The stream. /// The index of the message. - /// The labels to set. - /// If set to true, no events will be emitted. + /// The desired section of the message. /// The cancellation token. - Task SetLabelsAsync (int index, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + /// The progress reporting mechanism. + /// + /// is . + /// + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task GetStreamAsync (int index, string section, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Set the labels of the specified messages. + /// Get a substream of the specified message. /// /// - /// Sets the labels of the specified messages. + /// Gets a substream of the specified message. If the starting offset is beyond + /// the end of the specified section of the message, an empty stream is returned. If + /// the number of bytes desired extends beyond the end of the section, a truncated + /// stream will be returned. + /// For more information about how to construct the , + /// see Section 6.4.5 of RFC3501. /// - /// The indexes of the messages. - /// The labels to set. - /// If set to true, no events will be emitted. + /// The stream. + /// The index of the message. + /// The desired section of the message. + /// The starting offset of the first desired byte. + /// The number of bytes desired. /// The cancellation token. - void SetLabels (IList indexes, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + /// The progress reporting mechanism. + /// + /// is . + /// + /// + /// is out of range. + /// -or- + /// is negative. + /// -or- + /// is negative. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Stream GetStream (int index, string section, int offset, int count, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Asynchronously set the labels of the specified messages. + /// Asynchronously get a substream of the specified message. /// /// - /// Asynchronously sets the labels of the specified messages. + /// Asynchronously gets a substream of the specified message. If the starting + /// offset is beyond the end of the specified section of the message, an empty stream + /// is returned. If the number of bytes desired extends beyond the end of the section, + /// a truncated stream will be returned. + /// For more information about how to construct the , + /// see Section 6.4.5 of RFC3501. /// - /// An asynchronous task context. - /// The indexes of the messages. - /// The labels to set. - /// If set to true, no events will be emitted. + /// The stream. + /// The index of the message. + /// The desired section of the message. + /// The starting offset of the first desired byte. + /// The number of bytes desired. /// The cancellation token. - Task SetLabelsAsync (IList indexes, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Add a set of labels to the specified messages only if their mod-sequence value is less than the specified value. - /// - /// - /// Adds a set of labels to the specified messages only if their mod-sequence value is less than the specified value. + /// The progress reporting mechanism. + /// + /// is . + /// + /// + /// is out of range. + /// -or- + /// is negative. + /// -or- + /// is negative. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task GetStreamAsync (int index, string section, int offset, int count, CancellationToken cancellationToken = default, ITransferProgress? progress = null); + + /// + /// Store message flags and keywords for a message. + /// + /// + /// Updates the message flags and keywords for a message. + /// + /// if the store operation was successful; otherwise, . + /// The UID of the message. + /// The message flags and keywords to store. + /// The cancellation token. + /// + /// is invalid. + /// + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The specified an + /// value but the folder does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + bool Store (UniqueId uid, IStoreFlagsRequest request, CancellationToken cancellationToken = default); + + /// + /// Asynchronously store message flags and keywords for a message. + /// + /// + /// Asynchronously updates the message flags and keywords for a message. + /// + /// if the store operation was successful; otherwise, . + /// The UID of the message. + /// The message flags and keywords to store. + /// The cancellation token. + /// + /// is invalid. + /// + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The specified an + /// value but the folder does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task StoreAsync (UniqueId uid, IStoreFlagsRequest request, CancellationToken cancellationToken = default); + + /// + /// Store message flags and keywords for a set of messages. + /// + /// + /// Updates the message flags and keywords for a set of messages. + /// + /// The UIDs of the messages that were not updated. + /// The message UIDs. + /// The message flags and keywords to store. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The specified an + /// value but the folder does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + IList Store (IList uids, IStoreFlagsRequest request, CancellationToken cancellationToken = default); + + /// + /// Asynchronously store message flags and keywords for a set of messages. + /// + /// + /// Asynchronously updates the message flags and keywords for a set of messages. + /// + /// The UIDs of the messages that were not updated. + /// The message UIDs. + /// The message flags and keywords to store. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The specified an + /// value but the folder does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task> StoreAsync (IList uids, IStoreFlagsRequest request, CancellationToken cancellationToken = default); + + /// + /// Store message flags and keywords for a message. + /// + /// + /// Updates the message flags and keywords for a message. + /// + /// if the store operation was successful; otherwise, . + /// The index of the message. + /// The message flags and keywords to store. + /// The cancellation token. + /// + /// is invalid. + /// + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The specified an + /// value but the folder does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + bool Store (int index, IStoreFlagsRequest request, CancellationToken cancellationToken = default); + + /// + /// Asynchronously store message flags and keywords for a message. + /// + /// + /// Asynchronously updates the message flags and keywords for a message. + /// + /// if the store operation was successful; otherwise, . + /// The index of the message. + /// The message flags and keywords to store. + /// The cancellation token. + /// + /// is invalid. + /// + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The specified an + /// value but the folder does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task StoreAsync (int index, IStoreFlagsRequest request, CancellationToken cancellationToken = default); + + /// + /// Store message flags and keywords for a set of messages. + /// + /// + /// Updates the message flags and keywords for a set of messages. /// /// The indexes of the messages that were not updated. - /// The indexes of the messages. - /// The mod-sequence value. - /// The labels to add. - /// If set to true, no events will be emitted. - /// The cancellation token. - IList AddLabels (IList indexes, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously add a set of labels to the specified messages only if their mod-sequence value is less than the specified value. - /// - /// - /// Asynchronously adds a set of labels to the specified messages only if their mod-sequence value is less than the specified value. + /// The message indexes. + /// The message flags and keywords to store. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The specified an + /// value but the folder does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + IList Store (IList indexes, IStoreFlagsRequest request, CancellationToken cancellationToken = default); + + /// + /// Asynchronously store message flags and keywords for a set of messages. + /// + /// + /// Asynchronously updates the message flags and keywords for a set of message. /// /// The indexes of the messages that were not updated. - /// The indexes of the messages. - /// The mod-sequence value. - /// The labels to add. - /// If set to true, no events will be emitted. - /// The cancellation token. - Task> AddLabelsAsync (IList indexes, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Remove a set of labels from the specified messages only if their mod-sequence value is less than the specified value. - /// - /// - /// Removes a set of labels from the specified messages only if their mod-sequence value is less than the specified value. + /// The message indexes. + /// The message flags and keywords to store. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The specified an + /// value but the folder does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task> StoreAsync (IList indexes, IStoreFlagsRequest request, CancellationToken cancellationToken = default); + + /// + /// Store GMail-style labels for a message. + /// + /// + /// Updates the GMail-style labels for a message. + /// + /// if the store operation was successful; otherwise, . + /// The UID of the message. + /// The labels to store. + /// The cancellation token. + /// + /// is invalid. + /// + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The folder does not support storing labels. + /// -or- + /// The specified an + /// value but the folder does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + bool Store (UniqueId uid, IStoreLabelsRequest request, CancellationToken cancellationToken = default); + + /// + /// Asynchronously store GMail-style labels for a message. + /// + /// + /// Asynchronously updates the GMail-style labels for a message. + /// + /// if the store operation was successful; otherwise, . + /// The UID of the message. + /// The labels to store. + /// The cancellation token. + /// + /// is invalid. + /// + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The folder does not support storing labels. + /// -or- + /// The specified an + /// value but the folder does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task StoreAsync (UniqueId uid, IStoreLabelsRequest request, CancellationToken cancellationToken = default); + + /// + /// Store GMail-style labels for a set of messages. + /// + /// + /// Updates the GMail-style labels for a set of messages. + /// + /// The UIDs of the messages that were not updated. + /// The message UIDs. + /// The labels to store. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The folder does not support storing labels. + /// -or- + /// The specified an + /// value but the folder does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + IList Store (IList uids, IStoreLabelsRequest request, CancellationToken cancellationToken = default); + + /// + /// Asynchronously store GMail-style labels for a set of messages. + /// + /// + /// Asynchronously updates the GMail-style labels for a set of messages. + /// + /// The UIDs of the messages that were not updated. + /// The message UIDs. + /// The labels to store. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The folder does not support storing labels. + /// -or- + /// The specified an + /// value but the folder does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task> StoreAsync (IList uids, IStoreLabelsRequest request, CancellationToken cancellationToken = default); + + /// + /// Store GMail-style labels for a message. + /// + /// + /// Updates the GMail-style labels for a message. + /// + /// if the store operation was successful; otherwise, . + /// The index of the message. + /// The labels to store. + /// The cancellation token. + /// + /// is invalid. + /// + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The folder does not support storing labels. + /// -or- + /// The specified an + /// value but the folder does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + bool Store (int index, IStoreLabelsRequest request, CancellationToken cancellationToken = default); + + /// + /// Asynchronously store GMail-style labels for a message. + /// + /// + /// Asynchronously updates the GMail-style labels for a message. + /// + /// if the store operation was successful; otherwise, . + /// The index of the message. + /// The labels to store. + /// The cancellation token. + /// + /// is invalid. + /// + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The folder does not support storing labels. + /// -or- + /// The specified an + /// value but the folder does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task StoreAsync (int index, IStoreLabelsRequest request, CancellationToken cancellationToken = default); + + /// + /// Store GMail-style labels for a set of messages. + /// + /// + /// Updates the GMail-style labels for a set of messages. /// /// The indexes of the messages that were not updated. - /// The indexes of the messages. - /// The mod-sequence value. - /// The labels to remove. - /// If set to true, no events will be emitted. - /// The cancellation token. - IList RemoveLabels (IList indexes, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously remove a set of labels from the specified messages only if their mod-sequence value is less than the specified value. - /// - /// - /// Asynchronously removes a set of labels from the specified messages only if their mod-sequence value is less than the specified value. + /// The message indexes. + /// The labels to store. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The folder does not support storing labels. + /// -or- + /// The specified an + /// value but the folder does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + IList Store (IList indexes, IStoreLabelsRequest request, CancellationToken cancellationToken = default); + + /// + /// Asynchronously store GMail-style labels for a set of messages. + /// + /// + /// Asynchronously updates the GMail-style labels for a set of message. /// /// The indexes of the messages that were not updated. - /// The indexes of the messages. + /// The message indexes. + /// The labels to store. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The folder does not support storing labels. + /// -or- + /// The specified an + /// value but the folder does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task> StoreAsync (IList indexes, IStoreLabelsRequest request, CancellationToken cancellationToken = default); + + /// + /// Store the annotations for the specified message. + /// + /// + /// Stores the annotations for the specified message. + /// + /// The UID of the message. + /// The annotations to store. + /// The cancellation token. + /// + /// is . + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// Cannot store annotations without any properties defined. + /// + /// + /// The does not support annotations. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + void Store (UniqueId uid, IList annotations, CancellationToken cancellationToken = default); + + /// + /// Asynchronously store the annotations for the specified message. + /// + /// + /// Asynchronously stores the annotations for the specified message. + /// + /// An asynchronous task context. + /// The UID of the message. + /// The annotations to store. + /// The cancellation token. + /// + /// is . + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// Cannot store annotations without any properties defined. + /// + /// + /// The does not support annotations. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task StoreAsync (UniqueId uid, IList annotations, CancellationToken cancellationToken = default); + + /// + /// Store the annotations for the specified messages. + /// + /// + /// Stores the annotations for the specified messages. + /// + /// The UIDs of the messages. + /// The annotations to store. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// Cannot store annotations without any properties defined. + /// + /// + /// The does not support annotations. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + void Store (IList uids, IList annotations, CancellationToken cancellationToken = default); + + /// + /// Asynchronously store the annotations for the specified messages. + /// + /// + /// Asynchronously stores the annotations for the specified messages. + /// + /// An asynchronous task context. + /// The UIDs of the messages. + /// The annotations to store. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// Cannot store annotations without any properties defined. + /// + /// + /// The does not support annotations. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task StoreAsync (IList uids, IList annotations, CancellationToken cancellationToken = default); + + /// + /// Store the annotations for the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Stores the annotations for the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. /// The mod-sequence value. - /// The labels to remove. - /// If set to true, no events will be emitted. - /// The cancellation token. - Task> RemoveLabelsAsync (IList indexes, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Set the labels of the specified messages only if their mod-sequence value is less than the specified value. - /// - /// - /// Sets the labels of the specified messages only if their mod-sequence value is less than the specified value. + /// The annotations to store. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// Cannot store annotations without any properties defined. + /// + /// + /// The does not support annotations. + /// -or- + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + IList Store (IList uids, ulong modseq, IList annotations, CancellationToken cancellationToken = default); + + /// + /// Asynchronously store the annotations for the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously stores the annotations for the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The annotations to store. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// Cannot store annotations without any properties defined. + /// + /// + /// The does not support annotations. + /// -or- + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task> StoreAsync (IList uids, ulong modseq, IList annotations, CancellationToken cancellationToken = default); + + /// + /// Store the annotations for the specified message. + /// + /// + /// Stores the annotations for the specified message. + /// + /// The index of the message. + /// The annotations to store. + /// The cancellation token. + /// + /// is . + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// Cannot store annotations without any properties defined. + /// + /// + /// The does not support annotations. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + void Store (int index, IList annotations, CancellationToken cancellationToken = default); + + /// + /// Asynchronously store the annotations for the specified message. + /// + /// + /// Asynchronously stores the annotations for the specified message. + /// + /// An asynchronous task context. + /// The indexes of the message. + /// The annotations to store. + /// The cancellation token. + /// + /// is . + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// Cannot store annotations without any properties defined. + /// + /// + /// The does not support annotations. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task StoreAsync (int index, IList annotations, CancellationToken cancellationToken = default); + + /// + /// Store the annotations for the specified messages. + /// + /// + /// Stores the annotations for the specified messages. + /// + /// The indexes of the messages. + /// The annotations to store. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// Cannot store annotations without any properties defined. + /// + /// + /// The does not support annotations. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + void Store (IList indexes, IList annotations, CancellationToken cancellationToken = default); + + /// + /// Asynchronously store the annotations for the specified messages. + /// + /// + /// Asynchronously stores the annotations for the specified messages. + /// + /// An asynchronous task context. + /// The indexes of the messages. + /// The annotations to store. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// Cannot store annotations without any properties defined. + /// + /// + /// The does not support annotations. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task StoreAsync (IList indexes, IList annotations, CancellationToken cancellationToken = default); + + /// + /// Store the annotations for the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Stores the annotations for the specified messages only if their mod-sequence value is less than the specified value. /// /// The indexes of the messages that were not updated. /// The indexes of the messages. /// The mod-sequence value. - /// The labels to set. - /// If set to true, no events will be emitted. - /// The cancellation token. - IList SetLabels (IList indexes, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously set the labels of the specified messages only if their mod-sequence value is less than the specified value. - /// - /// - /// Asynchronously sets the labels of the specified messages only if their mod-sequence value is less than the specified value. + /// The annotations to store. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// Cannot store annotations without any properties defined. + /// + /// + /// The does not support annotations. + /// -or- + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + IList Store (IList indexes, ulong modseq, IList annotations, CancellationToken cancellationToken = default); + + /// + /// Asynchronously store the annotations for the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously stores the annotations for the specified messages only if their mod-sequence value is less than the specified value.s /// /// The indexes of the messages that were not updated. /// The indexes of the messages. /// The mod-sequence value. - /// The labels to set. - /// If set to true, no events will be emitted. - /// The cancellation token. - Task> SetLabelsAsync (IList indexes, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + /// The annotations to store. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// Cannot store annotations without any properties defined. + /// + /// + /// The does not support annotations. + /// -or- + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task> StoreAsync (IList indexes, ulong modseq, IList annotations, CancellationToken cancellationToken = default); /// /// Search the folder for messages matching the specified query. @@ -4157,7 +7903,37 @@ public interface IMailFolder : IEnumerable /// An array of matching UIDs. /// The search query. /// The cancellation token. - IList Search (SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is . + /// + /// + /// One or more search terms in the are not supported by the mail store. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + IList Search (SearchQuery query, CancellationToken cancellationToken = default); /// /// Asynchronously search the folder for messages matching the specified query. @@ -4169,36 +7945,37 @@ public interface IMailFolder : IEnumerable /// An array of matching UIDs. /// The search query. /// The cancellation token. - Task> SearchAsync (SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Search the folder for messages matching the specified query, returning them in the preferred sort order. - /// - /// - /// The returned array of unique identifiers will be sorted in the preferred order and - /// can be used with . - /// - /// An array of matching UIDs in the specified sort order. - /// The search query. - /// The sort order. - /// The cancellation token. - [Obsolete ("Use Sort(SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)) instead.")] - IList Search (SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously search the folder for messages matching the specified query, - /// returning them in the preferred sort order. - /// - /// - /// The returned array of unique identifiers will be sorted in the preferred order and - /// can be used with . - /// - /// An array of matching UIDs in the specified sort order. - /// The search query. - /// The sort order. - /// The cancellation token. - [Obsolete ("Use SortAsync(SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)) instead.")] - Task> SearchAsync (SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is . + /// + /// + /// One or more search terms in the are not supported by the mail store. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task> SearchAsync (SearchQuery query, CancellationToken cancellationToken = default); /// /// Search the subset of UIDs in the folder for messages matching the specified query. @@ -4211,7 +7988,44 @@ public interface IMailFolder : IEnumerable /// The subset of UIDs /// The search query. /// The cancellation token. - IList Search (IList uids, SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is . + /// -or- + /// is . + /// + /// + /// is empty. + /// -or- + /// One or more of the is invalid. + /// + /// + /// One or more search terms in the are not supported by the mail store. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + IList Search (IList uids, SearchQuery query, CancellationToken cancellationToken = default); /// /// Asynchronously search the subset of UIDs in the folder for messages matching the specified query. @@ -4224,39 +8038,44 @@ public interface IMailFolder : IEnumerable /// The subset of UIDs /// The search query. /// The cancellation token. - Task> SearchAsync (IList uids, SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Search the subset of UIDs in the folder for messages matching the specified query, - /// returning them in the preferred sort order. - /// - /// - /// The returned array of unique identifiers will be sorted in the preferred order and - /// can be used with . - /// - /// An array of matching UIDs in the specified sort order. - /// The subset of UIDs - /// The search query. - /// The sort order. - /// The cancellation token. - [Obsolete ("Use Sort(IList uids, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)) instead.")] - IList Search (IList uids, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously search the subset of UIDs in the folder for messages matching the specified query, - /// returning them in the preferred sort order. - /// - /// - /// The returned array of unique identifiers will be sorted in the preferred order and - /// can be used with . - /// - /// An array of matching UIDs in the specified sort order. - /// The subset of UIDs - /// The search query. - /// The sort order. - /// The cancellation token. - [Obsolete ("Use SortAsync(IList uids, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)) instead.")] - Task> SearchAsync (IList uids, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is . + /// -or- + /// is . + /// + /// + /// is empty. + /// -or- + /// One or more of the is invalid. + /// + /// + /// One or more search terms in the are not supported by the mail store. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task> SearchAsync (IList uids, SearchQuery query, CancellationToken cancellationToken = default); /// /// Search the folder for messages matching the specified query. @@ -4269,7 +8088,39 @@ public interface IMailFolder : IEnumerable /// The search options. /// The search query. /// The cancellation token. - SearchResults Search (SearchOptions options, SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is . + /// + /// + /// One or more search terms in the are not supported. + /// -or- + /// The server does not support the specified search options. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + SearchResults Search (SearchOptions options, SearchQuery query, CancellationToken cancellationToken = default); /// /// Asynchronously search the folder for messages matching the specified query. @@ -4282,59 +8133,45 @@ public interface IMailFolder : IEnumerable /// The search options. /// The search query. /// The cancellation token. - Task SearchAsync (SearchOptions options, SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Searches the folder for messages matching the specified query, - /// returning them in the preferred sort order. - /// - /// - /// Searches the folder for messages matching the specified query and ordering, - /// returning the search results in the specified sort order. - /// - /// The search results. - /// The search options. - /// The search query. - /// The sort order. - /// The cancellation token. - [Obsolete ("Use Sort(SearchOptions options, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)) instead.")] - SearchResults Search (SearchOptions options, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously searches the folder for messages matching the specified query, - /// returning them in the preferred sort order. - /// - /// - /// Asynchronously searches the folder for messages matching the specified query and ordering, - /// returning the search results in the specified sort order. - /// - /// The search results. - /// The search options. - /// The search query. - /// The sort order. - /// The cancellation token. - [Obsolete ("Use SortAsync(SearchOptions options, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)) instead.")] - Task SearchAsync (SearchOptions options, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Searches the subset of UIDs in the folder for messages matching the specified query. - /// - /// - /// Searches the fsubset of UIDs in the folder for messages matching the specified query, - /// returning only the specified search results. - /// - /// The search results. - /// The search options. - /// The subset of UIDs - /// The search query. - /// The cancellation token. - SearchResults Search (SearchOptions options, IList uids, SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is . + /// + /// + /// One or more search terms in the are not supported. + /// -or- + /// The server does not support the specified search options. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task SearchAsync (SearchOptions options, SearchQuery query, CancellationToken cancellationToken = default); /// - /// Asynchronously searches the subset of UIDs in the folder for messages matching the specified query. + /// Search the subset of UIDs in the folder for messages matching the specified query. /// /// - /// Asynchronously searches the fsubset of UIDs in the folder for messages matching the specified query, + /// Searches the subset of UIDs in the folder for messages matching the specified query, /// returning only the specified search results. /// /// The search results. @@ -4342,41 +8179,99 @@ public interface IMailFolder : IEnumerable /// The subset of UIDs /// The search query. /// The cancellation token. - Task SearchAsync (SearchOptions options, IList uids, SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is . + /// -or- + /// is . + /// + /// + /// is empty. + /// -or- + /// One or more of the is invalid. + /// + /// + /// One or more search terms in the are not supported. + /// -or- + /// The server does not support the specified search options. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + SearchResults Search (SearchOptions options, IList uids, SearchQuery query, CancellationToken cancellationToken = default); /// - /// Searches the subset of UIDs in the folder for messages matching the specified query, - /// returning them in the preferred sort order. + /// Asynchronously search the subset of UIDs in the folder for messages matching the specified query. /// /// - /// Searches the folder for messages matching the specified query and ordering, - /// returning the search results in the specified sort order. - /// - /// The search results. - /// The search options. - /// The subset of UIDs - /// The search query. - /// The sort order. - /// The cancellation token. - [Obsolete ("Use Sort(SearchOptions options, IList uids, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)) instead.")] - SearchResults Search (SearchOptions options, IList uids, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)); - - /// /// Asynchronously searches the subset of UIDs in the folder for messages matching the specified query, - /// returning them in the preferred sort order. - /// - /// - /// Asynchronously searches the folder for messages matching the specified query and ordering, - /// returning the search results in the specified sort order. + /// returning only the specified search results. /// /// The search results. /// The search options. /// The subset of UIDs /// The search query. - /// The sort order. /// The cancellation token. - [Obsolete ("Use SortAsync(SearchOptions options, IList uids, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)) instead.")] - Task SearchAsync (SearchOptions options, IList uids, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is . + /// -or- + /// is . + /// + /// + /// is empty. + /// -or- + /// One or more of the is invalid. + /// + /// + /// One or more search terms in the are not supported. + /// -or- + /// The server does not support the specified search options. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task SearchAsync (SearchOptions options, IList uids, SearchQuery query, CancellationToken cancellationToken = default); /// /// Sort messages matching the specified query. @@ -4389,7 +8284,44 @@ public interface IMailFolder : IEnumerable /// The search query. /// The sort order. /// The cancellation token. - IList Sort (SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is . + /// -or- + /// is . + /// + /// + /// is empty. + /// + /// + /// One or more search terms in the are not supported. + /// -or- + /// The server does not support sorting search results. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + IList Sort (SearchQuery query, IList orderBy, CancellationToken cancellationToken = default); /// /// Asynchronously sort messages matching the specified query. @@ -4402,7 +8334,44 @@ public interface IMailFolder : IEnumerable /// The search query. /// The sort order. /// The cancellation token. - Task> SortAsync (SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is . + /// -or- + /// is . + /// + /// + /// is empty. + /// + /// + /// One or more search terms in the are not supported. + /// -or- + /// The server does not support sorting search results. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task> SortAsync (SearchQuery query, IList orderBy, CancellationToken cancellationToken = default); /// /// Sort messages matching the specified query. @@ -4416,7 +8385,50 @@ public interface IMailFolder : IEnumerable /// The search query. /// The sort order. /// The cancellation token. - IList Sort (IList uids, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is . + /// -or- + /// is . + /// -or- + /// is . + /// + /// + /// is empty. + /// -or- + /// One or more of the is invalid. + /// -or- + /// is empty. + /// + /// + /// One or more search terms in the are not supported. + /// -or- + /// The server does not support sorting search results. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + IList Sort (IList uids, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default); /// /// Asynchronously sort messages matching the specified query. @@ -4430,7 +8442,50 @@ public interface IMailFolder : IEnumerable /// The search query. /// The sort order. /// The cancellation token. - Task> SortAsync (IList uids, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is . + /// -or- + /// is . + /// -or- + /// is . + /// + /// + /// is empty. + /// -or- + /// One or more of the is invalid. + /// -or- + /// is empty. + /// + /// + /// One or more search terms in the are not supported. + /// -or- + /// The server does not support sorting search results. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task> SortAsync (IList uids, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default); /// /// Sort messages matching the specified query. @@ -4443,7 +8498,46 @@ public interface IMailFolder : IEnumerable /// The search query. /// The sort order. /// The cancellation token. - SearchResults Sort (SearchOptions options, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is . + /// -or- + /// is . + /// + /// + /// is empty. + /// + /// + /// One or more search terms in the are not supported. + /// -or- + /// The server does not support the specified search options. + /// -or- + /// The server does not support sorting search results. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + SearchResults Sort (SearchOptions options, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default); /// /// Asynchronously sort messages matching the specified query. @@ -4456,7 +8550,46 @@ public interface IMailFolder : IEnumerable /// The search query. /// The sort order. /// The cancellation token. - Task SortAsync (SearchOptions options, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is . + /// -or- + /// is . + /// + /// + /// is empty. + /// + /// + /// One or more search terms in the are not supported. + /// -or- + /// The server does not support the specified search options. + /// -or- + /// The server does not support sorting search results. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task SortAsync (SearchOptions options, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default); /// /// Sort messages matching the specified query. @@ -4470,7 +8603,52 @@ public interface IMailFolder : IEnumerable /// The search query. /// The sort order. /// The cancellation token. - SearchResults Sort (SearchOptions options, IList uids, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is . + /// -or- + /// is . + /// -or- + /// is . + /// + /// + /// is empty. + /// -or- + /// One or more of the is invalid. + /// -or- + /// is empty. + /// + /// + /// One or more search terms in the are not supported. + /// -or- + /// The server does not support the specified search options. + /// -or- + /// The server does not support sorting search results. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + SearchResults Sort (SearchOptions options, IList uids, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default); /// /// Asynchronously sort messages matching the specified query. @@ -4485,7 +8663,52 @@ public interface IMailFolder : IEnumerable /// The search query. /// The sort order. /// The cancellation token. - Task SortAsync (SearchOptions options, IList uids, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is . + /// -or- + /// is . + /// -or- + /// is . + /// + /// + /// is empty. + /// -or- + /// One or more of the is invalid. + /// -or- + /// is empty. + /// + /// + /// One or more search terms in the are not supported. + /// -or- + /// The server does not support the specified search options. + /// -or- + /// The server does not support sorting search results. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task SortAsync (SearchOptions options, IList uids, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default); /// /// Thread the messages in the folder that match the search query using the specified threading algorithm. @@ -4498,7 +8721,42 @@ public interface IMailFolder : IEnumerable /// The threading algorithm to use. /// The search query. /// The cancellation token. - IList Thread (ThreadingAlgorithm algorithm, SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is not supported. + /// + /// + /// is . + /// + /// + /// One or more search terms in the are not supported. + /// -or- + /// The server does not support threading search results. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + IList Thread (ThreadingAlgorithm algorithm, SearchQuery query, CancellationToken cancellationToken = default); /// /// Asynchronously thread the messages in the folder that match the search query using the specified threading algorithm. @@ -4511,7 +8769,42 @@ public interface IMailFolder : IEnumerable /// The threading algorithm to use. /// The search query. /// The cancellation token. - Task> ThreadAsync (ThreadingAlgorithm algorithm, SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is not supported. + /// + /// + /// is . + /// + /// + /// One or more search terms in the are not supported. + /// -or- + /// The server does not support threading search results. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task> ThreadAsync (ThreadingAlgorithm algorithm, SearchQuery query, CancellationToken cancellationToken = default); /// /// Thread the messages in the folder that match the search query using the specified threading algorithm. @@ -4525,7 +8818,49 @@ public interface IMailFolder : IEnumerable /// The threading algorithm to use. /// The search query. /// The cancellation token. - IList Thread (IList uids, ThreadingAlgorithm algorithm, SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is not supported. + /// + /// + /// is . + /// -or- + /// is . + /// + /// + /// is empty. + /// -or- + /// One or more of the is invalid. + /// + /// + /// One or more search terms in the are not supported. + /// -or- + /// The server does not support threading search results. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + IList Thread (IList uids, ThreadingAlgorithm algorithm, SearchQuery query, CancellationToken cancellationToken = default); /// /// Asynchronously thread the messages in the folder that match the search query using the specified threading algorithm. @@ -4539,7 +8874,49 @@ public interface IMailFolder : IEnumerable /// The threading algorithm to use. /// The search query. /// The cancellation token. - Task> ThreadAsync (IList uids, ThreadingAlgorithm algorithm, SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)); + /// + /// is not supported. + /// + /// + /// is . + /// -or- + /// is . + /// + /// + /// is empty. + /// -or- + /// One or more of the is invalid. + /// + /// + /// One or more search terms in the are not supported. + /// -or- + /// The server does not support threading search results. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + Task> ThreadAsync (IList uids, ThreadingAlgorithm algorithm, SearchQuery query, CancellationToken cancellationToken = default); /// /// Occurs when the folder is opened. @@ -4595,16 +8972,11 @@ public interface IMailFolder : IEnumerable /// /// Emitted when a message is expunged from the folder. /// + /// + /// + /// event EventHandler MessageExpunged; - /// - /// Occurs when new messages arrive in the folder. - /// - /// - /// Emitted when new mmessages arrive in the folder. - /// - event EventHandler MessagesArrived; - /// /// Occurs when messages vanish from the folder. /// @@ -4619,6 +8991,9 @@ public interface IMailFolder : IEnumerable /// /// Emitted when flags changed on a message. /// + /// + /// + /// event EventHandler MessageFlagsChanged; /// @@ -4629,6 +9004,14 @@ public interface IMailFolder : IEnumerable /// event EventHandler MessageLabelsChanged; + /// + /// Occurs when annotations changed on a message. + /// + /// + /// Emitted when annotations changed on a message. + /// + event EventHandler AnnotationsChanged; + /// /// Occurs when a message summary is fetched from the folder. /// @@ -4644,11 +9027,19 @@ public interface IMailFolder : IEnumerable /// The Fetch /// methods will return a list of all message summaries that any information was /// retrieved for, regardless of whether or not all of the requested items were fetched, - /// therefore there may be a discrepency between the number of times this event is - /// emitetd and the number of summary items returned from the Fetch method. + /// therefore there may be a discrepancy between the number of times this event is + /// emitted and the number of summary items returned from the Fetch method. /// event EventHandler MessageSummaryFetched; + /// + /// Occurs when metadata changes. + /// + /// + /// The event is emitted when metadata changes. + /// + event EventHandler MetadataChanged; + /// /// Occurs when the mod-sequence changed on a message. /// @@ -4657,28 +9048,71 @@ public interface IMailFolder : IEnumerable /// event EventHandler ModSeqChanged; + /// + /// Occurs when the highest mod-sequence changes. + /// + /// + /// The event is emitted whenever the value changes. + /// + event EventHandler HighestModSeqChanged; + + /// + /// Occurs when the next UID changes. + /// + /// + /// Emitted when the property changes. + /// + event EventHandler UidNextChanged; + /// /// Occurs when the UID validity changes. /// /// - /// Emitted when the UID validity changes. + /// Emitted when the property changes. /// event EventHandler UidValidityChanged; + /// + /// Occurs when the ID changes. + /// + /// + /// Emitted when the property changes. + /// + event EventHandler IdChanged; + + /// + /// Occurs when the size of the folder changes. + /// + /// + /// Emitted when the property changes. + /// + event EventHandler SizeChanged; + /// /// Occurs when the message count changes. /// /// - /// Emitted when the message count changes. + /// Emitted when the property changes. /// + /// + /// + /// event EventHandler CountChanged; /// /// Occurs when the recent message count changes. /// /// - /// Emitted when the recent message count changes. + /// Emitted when the property changes. /// event EventHandler RecentChanged; + + /// + /// Occurs when the message unread count changes. + /// + /// + /// Emitted when the property changes. + /// + event EventHandler UnreadChanged; } } diff --git a/MailKit/IMailFolderAppendExtensions.cs b/MailKit/IMailFolderAppendExtensions.cs new file mode 100644 index 0000000000..37a648f382 --- /dev/null +++ b/MailKit/IMailFolderAppendExtensions.cs @@ -0,0 +1,2177 @@ +// +// IMailFolderAppendExtensions.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; + +using MimeKit; + +namespace MailKit { + public static partial class IMailFolderExtensions + { + #region Append Extensions + + /// + /// Append the specified message to the folder. + /// + /// + /// Appends the specified message to the folder and returns the UniqueId assigned to the message. + /// + /// The UID of the appended message, if available; otherwise, . + /// The folder. + /// The message. + /// The message flags. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static UniqueId? Append (this IMailFolder folder, MimeMessage message, MessageFlags flags = MessageFlags.None, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + return Append (folder, FormatOptions.Default, message, flags, cancellationToken, progress); + } + + /// + /// Asynchronously append the specified message to the folder. + /// + /// + /// Asynchronously appends the specified message to the folder and returns the UniqueId assigned to the message. + /// + /// The UID of the appended message, if available; otherwise, . + /// The folder. + /// The message. + /// The message flags. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task AppendAsync (this IMailFolder folder, MimeMessage message, MessageFlags flags = MessageFlags.None, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + return AppendAsync (folder, FormatOptions.Default, message, flags, cancellationToken, progress); + } + + /// + /// Append the specified message to the folder. + /// + /// + /// Appends the specified message to the folder and returns the UniqueId assigned to the message. + /// + /// The UID of the appended message, if available; otherwise, . + /// The folder. + /// The message. + /// The message flags. + /// The received date of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static UniqueId? Append (this IMailFolder folder, MimeMessage message, MessageFlags flags, DateTimeOffset date, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + return Append (folder, FormatOptions.Default, message, flags, date, cancellationToken, progress); + } + + /// + /// Asynchronously append the specified message to the folder. + /// + /// + /// Asynchronously appends the specified message to the folder and returns the UniqueId assigned to the message. + /// + /// The UID of the appended message, if available; otherwise, . + /// The folder. + /// The message. + /// The message flags. + /// The received date of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task AppendAsync (this IMailFolder folder, MimeMessage message, MessageFlags flags, DateTimeOffset date, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + return AppendAsync (folder, FormatOptions.Default, message, flags, date, cancellationToken, progress); + } + + /// + /// Append the specified message to the folder. + /// + /// + /// Appends the specified message to the folder and returns the UniqueId assigned to the message. + /// + /// The UID of the appended message, if available; otherwise, . + /// The folder. + /// The message. + /// The message flags. + /// The received date of the message. + /// The message annotations. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// One or more does not define any properties. + /// " + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static UniqueId? Append (this IMailFolder folder, MimeMessage message, MessageFlags flags, DateTimeOffset? date, IList annotations, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + return Append (folder, FormatOptions.Default, message, flags, date, annotations, cancellationToken, progress); + } + + /// + /// Asynchronously append the specified message to the folder. + /// + /// + /// Asynchronously appends the specified message to the folder and returns the UniqueId assigned to the message. + /// + /// The UID of the appended message, if available; otherwise, . + /// The folder. + /// The message. + /// The message flags. + /// The received date of the message. + /// The message annotations. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// One or more does not define any properties. + /// " + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task AppendAsync (this IMailFolder folder, MimeMessage message, MessageFlags flags, DateTimeOffset? date, IList annotations, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + return AppendAsync (folder, FormatOptions.Default, message, flags, date, annotations, cancellationToken, progress); + } + + /// + /// Append the specified message to the folder. + /// + /// + /// Appends the specified message to the folder and returns the UniqueId assigned to the message. + /// + /// The UID of the appended message, if available; otherwise, . + /// The folder. + /// The formatting options. + /// The message. + /// The message flags. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// -or- + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static UniqueId? Append (this IMailFolder folder, FormatOptions options, MimeMessage message, MessageFlags flags = MessageFlags.None, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + var request = new AppendRequest (message, flags) { + TransferProgress = progress + }; + + return folder.Append (options, request, cancellationToken); + } + + /// + /// Asynchronously append the specified message to the folder. + /// + /// + /// Asynchronously appends the specified message to the folder and returns the UniqueId assigned to the message. + /// + /// The UID of the appended message, if available; otherwise, . + /// The folder. + /// The formatting options. + /// The message. + /// The message flags. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// -or- + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task AppendAsync (this IMailFolder folder, FormatOptions options, MimeMessage message, MessageFlags flags = MessageFlags.None, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + var request = new AppendRequest (message, flags) { + TransferProgress = progress + }; + + return folder.AppendAsync (options, request, cancellationToken); + } + + /// + /// Append the specified message to the folder. + /// + /// + /// Appends the specified message to the folder and returns the UniqueId assigned to the message. + /// + /// The UID of the appended message, if available; otherwise, . + /// The folder. + /// The formatting options. + /// The message. + /// The message flags. + /// The received date of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// -or- + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static UniqueId? Append (this IMailFolder folder, FormatOptions options, MimeMessage message, MessageFlags flags, DateTimeOffset date, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + var request = new AppendRequest (message, flags, date) { + TransferProgress = progress + }; + + return folder.Append (options, request, cancellationToken); + } + + /// + /// Asynchronously append the specified message to the folder. + /// + /// + /// Asynchronously appends the specified message to the folder and returns the UniqueId assigned to the message. + /// + /// The UID of the appended message, if available; otherwise, . + /// The folder. + /// The formatting options. + /// The message. + /// The message flags. + /// The received date of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// -or- + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task AppendAsync (this IMailFolder folder, FormatOptions options, MimeMessage message, MessageFlags flags, DateTimeOffset date, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + var request = new AppendRequest (message, flags, date) { + TransferProgress = progress + }; + + return folder.AppendAsync (options, request, cancellationToken); + } + + /// + /// Append the specified message to the folder. + /// + /// + /// Appends the specified message to the folder and returns the UniqueId assigned to the message. + /// + /// The UID of the appended message, if available; otherwise, . + /// The folder. + /// The formatting options. + /// The message. + /// The message flags. + /// The received date of the message. + /// The message annotations. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// -or- + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static UniqueId? Append (this IMailFolder folder, FormatOptions options, MimeMessage message, MessageFlags flags, DateTimeOffset? date, IList annotations, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + var request = new AppendRequest (message, flags) { + TransferProgress = progress, + Annotations = annotations, + InternalDate = date + }; + + return folder.Append (options, request, cancellationToken); + } + + /// + /// Asynchronously append the specified message to the folder. + /// + /// + /// Asynchronously appends the specified message to the folder and returns the UniqueId assigned to the message. + /// + /// The UID of the appended message, if available; otherwise, . + /// The folder. + /// The formatting options. + /// The message. + /// The message flags. + /// The received date of the message. + /// The message annotations. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// -or- + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task AppendAsync (this IMailFolder folder, FormatOptions options, MimeMessage message, MessageFlags flags, DateTimeOffset? date, IList annotations, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + var request = new AppendRequest (message, flags) { + TransferProgress = progress, + Annotations = annotations, + InternalDate = date + }; + + return folder.AppendAsync (options, request, cancellationToken); + } + + /// + /// Append the specified messages to the folder. + /// + /// + /// Appends the specified messages to the folder and returns the UniqueIds assigned to the messages. + /// + /// The UIDs of the appended messages, if available; otherwise an empty array. + /// The folder. + /// The array of messages to append to the folder. + /// The message flags to use for each message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is null. + /// -or- + /// The number of messages does not match the number of flags. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static IList Append (this IMailFolder folder, IList messages, IList flags, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + return Append (folder, FormatOptions.Default, messages, flags, cancellationToken, progress); + } + + /// + /// Asynchronously append the specified messages to the folder. + /// + /// + /// Asynchronously appends the specified messages to the folder and returns the UniqueIds assigned to the messages. + /// + /// The UIDs of the appended messages, if available; otherwise an empty array. + /// The folder. + /// The array of messages to append to the folder. + /// The message flags to use for each message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is null. + /// -or- + /// The number of messages does not match the number of flags. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task> AppendAsync (this IMailFolder folder, IList messages, IList flags, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + return AppendAsync (folder, FormatOptions.Default, messages, flags, cancellationToken, progress); + } + + /// + /// Append the specified messages to the folder. + /// + /// + /// Appends the specified messages to the folder and returns the UniqueIds assigned to the messages. + /// + /// The UIDs of the appended messages, if available; otherwise an empty array. + /// The folder. + /// The array of messages to append to the folder. + /// The message flags to use for each of the messages. + /// The received dates to use for each of the messages. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// -or- + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is null. + /// -or- + /// The number of messages, flags, and dates do not match. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static IList Append (this IMailFolder folder, IList messages, IList flags, IList dates, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + return Append (folder, FormatOptions.Default, messages, flags, dates, cancellationToken, progress); + } + + /// + /// Asynchronously append the specified messages to the folder. + /// + /// + /// Asynchronously appends the specified messages to the folder and returns the UniqueIds assigned to the messages. + /// + /// The UIDs of the appended messages, if available; otherwise an empty array. + /// The folder. + /// The array of messages to append to the folder. + /// The message flags to use for each of the messages. + /// The received dates to use for each of the messages. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// -or- + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is null. + /// -or- + /// The number of messages, flags, and dates do not match. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task> AppendAsync (this IMailFolder folder, IList messages, IList flags, IList dates, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + return AppendAsync (folder, FormatOptions.Default, messages, flags, dates, cancellationToken, progress); + } + + /// + /// Append the specified messages to the folder. + /// + /// + /// Appends the specified messages to the folder and returns the UniqueIds assigned to the messages. + /// + /// The UIDs of the appended messages, if available; otherwise an empty array. + /// The folder. + /// The formatting options. + /// The array of messages to append to the folder. + /// The message flags to use for each message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// -or- + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is null. + /// -or- + /// The number of messages does not match the number of flags. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static IList Append (this IMailFolder folder, FormatOptions options, IList messages, IList flags, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (messages == null) + throw new ArgumentNullException (nameof (messages)); + + for (int i = 0; i < messages.Count; i++) { + if (messages[i] == null) + throw new ArgumentException ("One or more of the messages is null."); + } + + if (flags == null) + throw new ArgumentNullException (nameof (flags)); + + if (messages.Count != flags.Count) + throw new ArgumentException ("The number of messages and the number of flags must be equal."); + + var requests = new AppendRequest[messages.Count]; + for (int i = 0; i < messages.Count; i++) { + requests[i] = new AppendRequest (messages[i], flags[i]) { + TransferProgress = progress + }; + } + + return folder.Append (options, requests, cancellationToken); + } + + /// + /// Asynchronously append the specified messages to the folder. + /// + /// + /// Asynchronously appends the specified messages to the folder and returns the UniqueIds assigned to the messages. + /// + /// The UIDs of the appended messages, if available; otherwise an empty array. + /// The folder. + /// The formatting options. + /// The array of messages to append to the folder. + /// The message flags to use for each message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// -or- + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is null. + /// -or- + /// The number of messages does not match the number of flags. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task> AppendAsync (this IMailFolder folder, FormatOptions options, IList messages, IList flags, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (messages == null) + throw new ArgumentNullException (nameof (messages)); + + for (int i = 0; i < messages.Count; i++) { + if (messages[i] == null) + throw new ArgumentException ("One or more of the messages is null."); + } + + if (flags == null) + throw new ArgumentNullException (nameof (flags)); + + if (messages.Count != flags.Count) + throw new ArgumentException ("The number of messages and the number of flags must be equal."); + + var requests = new AppendRequest[messages.Count]; + for (int i = 0; i < messages.Count; i++) { + requests[i] = new AppendRequest (messages[i], flags[i]) { + TransferProgress = progress + }; + } + + return folder.AppendAsync (options, requests, cancellationToken); + } + + /// + /// Append the specified messages to the folder. + /// + /// + /// Appends the specified messages to the folder and returns the UniqueIds assigned to the messages. + /// + /// The UIDs of the appended messages, if available; otherwise an empty array. + /// The folder. + /// The formatting options. + /// The array of messages to append to the folder. + /// The message flags to use for each of the messages. + /// The received dates to use for each of the messages. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// -or- + /// is . + /// -or- + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is null. + /// -or- + /// The number of messages, flags, and dates do not match. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static IList Append (this IMailFolder folder, FormatOptions options, IList messages, IList flags, IList dates, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (messages == null) + throw new ArgumentNullException (nameof (messages)); + + for (int i = 0; i < messages.Count; i++) { + if (messages[i] == null) + throw new ArgumentException ("One or more of the messages is null."); + } + + if (flags == null) + throw new ArgumentNullException (nameof (flags)); + + if (messages.Count != flags.Count) + throw new ArgumentException ("The number of messages and the number of flags must be equal."); + + if (dates == null) + throw new ArgumentNullException (nameof (dates)); + + if (messages.Count != dates.Count) + throw new ArgumentException ("The number of messages and the number of dates must be equal."); + + var requests = new AppendRequest[messages.Count]; + for (int i = 0; i < messages.Count; i++) { + requests[i] = new AppendRequest (messages[i], flags[i], dates[i]) { + TransferProgress = progress + }; + } + + return folder.Append (options, requests, cancellationToken); + } + + /// + /// Asynchronously append the specified messages to the folder. + /// + /// + /// Asynchronously appends the specified messages to the folder and returns the UniqueIds assigned to the messages. + /// + /// The UIDs of the appended messages, if available; otherwise an empty array. + /// The folder. + /// The formatting options. + /// The array of messages to append to the folder. + /// The message flags to use for each of the messages. + /// The received dates to use for each of the messages. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// -or- + /// is . + /// -or- + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is null. + /// -or- + /// The number of messages, flags, and dates do not match. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task> AppendAsync (this IMailFolder folder, FormatOptions options, IList messages, IList flags, IList dates, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (messages == null) + throw new ArgumentNullException (nameof (messages)); + + for (int i = 0; i < messages.Count; i++) { + if (messages[i] == null) + throw new ArgumentException ("One or more of the messages is null."); + } + + if (flags == null) + throw new ArgumentNullException (nameof (flags)); + + if (messages.Count != flags.Count) + throw new ArgumentException ("The number of messages and the number of flags must be equal."); + + if (dates == null) + throw new ArgumentNullException (nameof (dates)); + + if (messages.Count != dates.Count) + throw new ArgumentException ("The number of messages and the number of dates must be equal."); + + var requests = new AppendRequest[messages.Count]; + for (int i = 0; i < messages.Count; i++) { + requests[i] = new AppendRequest (messages[i], flags[i], dates[i]) { + TransferProgress = progress + }; + } + + return folder.AppendAsync (options, requests, cancellationToken); + } + + #endregion Append Extensions + + #region Replace Extensions + + /// + /// Replace a message in the folder. + /// + /// + /// Replaces the specified message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, . + /// The folder. + /// The UID of the message to be replaced. + /// The message. + /// The message flags. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public static UniqueId? Replace (this IMailFolder folder, UniqueId uid, MimeMessage message, MessageFlags flags = MessageFlags.None, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + return Replace (folder, FormatOptions.Default, uid, message, flags, cancellationToken, progress); + } + + /// + /// Asynchronously replace a message in the folder. + /// + /// + /// Asynchronously replaces the specified message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, . + /// The folder. + /// The UID of the message to be replaced. + /// The message. + /// The message flags. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public static Task ReplaceAsync (this IMailFolder folder, UniqueId uid, MimeMessage message, MessageFlags flags = MessageFlags.None, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + return ReplaceAsync (folder, FormatOptions.Default, uid, message, flags, cancellationToken, progress); + } + + /// + /// Replace a message in the folder. + /// + /// + /// Replaces the specified message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, . + /// The folder. + /// The UID of the message to be replaced. + /// The message. + /// The message flags. + /// The received date of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public static UniqueId? Replace (this IMailFolder folder, UniqueId uid, MimeMessage message, MessageFlags flags, DateTimeOffset date, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + return Replace (folder, FormatOptions.Default, uid, message, flags, date, cancellationToken, progress); + } + + /// + /// Asynchronously replace a message in the folder. + /// + /// + /// Asynchronously replaces the specified message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, . + /// The folder. + /// The UID of the message to be replaced. + /// The message. + /// The message flags. + /// The received date of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public static Task ReplaceAsync (this IMailFolder folder, UniqueId uid, MimeMessage message, MessageFlags flags, DateTimeOffset date, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + return ReplaceAsync (folder, FormatOptions.Default, uid, message, flags, date, cancellationToken, progress); + } + + /// + /// Replace a message in the folder. + /// + /// + /// Replaces the specified message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, . + /// The folder. + /// The formatting options. + /// The UID of the message to be replaced. + /// The message. + /// The message flags. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// -or- + /// is . + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public static UniqueId? Replace (this IMailFolder folder, FormatOptions options, UniqueId uid, MimeMessage message, MessageFlags flags = MessageFlags.None, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + var request = new ReplaceRequest (message, flags) { + TransferProgress = progress + }; + + return folder.Replace (options, uid, request, cancellationToken); + } + + /// + /// Asynchronously replace a message in the folder. + /// + /// + /// Replaces the specified message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, . + /// The folder. + /// The formatting options. + /// The UID of the message to be replaced. + /// The message. + /// The message flags. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// -or- + /// is . + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public static Task ReplaceAsync (this IMailFolder folder, FormatOptions options, UniqueId uid, MimeMessage message, MessageFlags flags = MessageFlags.None, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + var request = new ReplaceRequest (message, flags) { + TransferProgress = progress + }; + + return folder.ReplaceAsync (options, uid, request, cancellationToken); + } + + /// + /// Replace a message in the folder. + /// + /// + /// Replaces the specified message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, . + /// The folder. + /// The formatting options. + /// The UID of the message to be replaced. + /// The message. + /// The message flags. + /// The received date of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// -or- + /// is . + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public static UniqueId? Replace (this IMailFolder folder, FormatOptions options, UniqueId uid, MimeMessage message, MessageFlags flags, DateTimeOffset date, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + var request = new ReplaceRequest (message, flags, date) { + TransferProgress = progress + }; + + return folder.Replace (options, uid, request, cancellationToken); + } + + /// + /// Asynchronously replace a message in the folder. + /// + /// + /// Asynchronously replaces the specified message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, . + /// The folder. + /// The formatting options. + /// The UID of the message to be replaced. + /// The message. + /// The message flags. + /// The received date of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// -or- + /// is . + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public static Task ReplaceAsync (this IMailFolder folder, FormatOptions options, UniqueId uid, MimeMessage message, MessageFlags flags, DateTimeOffset date, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + var request = new ReplaceRequest (message, flags, date) { + TransferProgress = progress + }; + + return folder.ReplaceAsync (options, uid, request, cancellationToken); + } + + /// + /// Replace a message in the folder. + /// + /// + /// Replaces the specified message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, . + /// The folder. + /// The index of the message to be replaced. + /// The message. + /// The message flags. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public static UniqueId? Replace (this IMailFolder folder, int index, MimeMessage message, MessageFlags flags = MessageFlags.None, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + return Replace (folder, FormatOptions.Default, index, message, flags, cancellationToken, progress); + } + + /// + /// Asynchronously replace a message in the folder. + /// + /// + /// Asynchronously replaces the specified message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, . + /// The folder. + /// The index of the message to be replaced. + /// The message. + /// The message flags. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public static Task ReplaceAsync (this IMailFolder folder, int index, MimeMessage message, MessageFlags flags = MessageFlags.None, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + return ReplaceAsync (folder, FormatOptions.Default, index, message, flags, cancellationToken, progress); + } + + /// + /// Replace a message in the folder. + /// + /// + /// Replaces the specified message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, . + /// The folder. + /// The index of the message to be replaced. + /// The message. + /// The message flags. + /// The received date of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public static UniqueId? Replace (this IMailFolder folder, int index, MimeMessage message, MessageFlags flags, DateTimeOffset date, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + return Replace (folder, FormatOptions.Default, index, message, flags, date, cancellationToken, progress); + } + + /// + /// Asynchronously replace a message in the folder. + /// + /// + /// Asynchronously replaces the specified message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, . + /// The folder. + /// The index of the message to be replaced. + /// The message. + /// The message flags. + /// The received date of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public static Task ReplaceAsync (this IMailFolder folder, int index, MimeMessage message, MessageFlags flags, DateTimeOffset date, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + return ReplaceAsync (folder, FormatOptions.Default, index, message, flags, date, cancellationToken, progress); + } + + /// + /// Replace a message in the folder. + /// + /// + /// Replaces the specified message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, . + /// The folder. + /// The formatting options. + /// The index of the message to be replaced. + /// The message. + /// The message flags. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// -or- + /// is . + /// + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public static UniqueId? Replace (this IMailFolder folder, FormatOptions options, int index, MimeMessage message, MessageFlags flags = MessageFlags.None, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + var request = new ReplaceRequest (message, flags) { + TransferProgress = progress + }; + + return folder.Replace (options, index, request, cancellationToken); + } + + /// + /// Asynchronously replace a message in the folder. + /// + /// + /// Asynchronously replaces the specified message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, . + /// The folder. + /// The formatting options. + /// The index of the message to be replaced. + /// The message. + /// The message flags. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// -or- + /// is . + /// + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public static Task ReplaceAsync (this IMailFolder folder, FormatOptions options, int index, MimeMessage message, MessageFlags flags = MessageFlags.None, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + var request = new ReplaceRequest (message, flags) { + TransferProgress = progress + }; + + return folder.ReplaceAsync (options, index, request, cancellationToken); + } + + /// + /// Replace a message in the folder. + /// + /// + /// Replaces the specified message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, . + /// The folder. + /// The formatting options. + /// The index of the message to be replaced. + /// The message. + /// The message flags. + /// The received date of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// -or- + /// is . + /// + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public static UniqueId? Replace (this IMailFolder folder, FormatOptions options, int index, MimeMessage message, MessageFlags flags, DateTimeOffset date, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + var request = new ReplaceRequest (message, flags, date) { + TransferProgress = progress + }; + + return folder.Replace (options, index, request, cancellationToken); + } + + /// + /// Asynchronously replace a message in the folder. + /// + /// + /// Replaces the specified message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, . + /// The folder. + /// The formatting options. + /// The index of the message to be replaced. + /// The message. + /// The message flags. + /// The received date of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// -or- + /// is . + /// + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public static Task ReplaceAsync (this IMailFolder folder, FormatOptions options, int index, MimeMessage message, MessageFlags flags, DateTimeOffset date, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + var request = new ReplaceRequest (message, flags, date) { + TransferProgress = progress + }; + + return folder.ReplaceAsync (options, index, request, cancellationToken); + } + + #endregion Replace Extensions + } +} diff --git a/MailKit/IMailFolderFetchExtensions.cs b/MailKit/IMailFolderFetchExtensions.cs new file mode 100644 index 0000000000..8569f03ba2 --- /dev/null +++ b/MailKit/IMailFolderFetchExtensions.cs @@ -0,0 +1,2219 @@ +// +// IMailFolderFetchExtensions.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; + +using MimeKit; + +namespace MailKit { + /// + /// Extension methods for that provide backwards API compatibility. + /// + /// + /// Extension methods for that provide backwards API compatibility. + /// + public static partial class IMailFolderExtensions + { + /// + /// Fetch the message summaries for the specified message UIDs. + /// + /// + /// Fetches the message summaries for the specified message UIDs. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// + /// + /// + /// An enumeration of summaries for the requested messages. + /// The folder. + /// The UIDs. + /// The message summary items to fetch. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static IList Fetch (this IMailFolder folder, IList uids, MessageSummaryItems items, CancellationToken cancellationToken = default) + { + var request = new FetchRequest (items); + + return folder.Fetch (uids, request, cancellationToken); + } + + /// + /// Asynchronously fetch the message summaries for the specified message UIDs. + /// + /// + /// Asynchronously fetches the message summaries for the specified message + /// UIDs. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// + /// + /// + /// An enumeration of summaries for the requested messages. + /// The folder. + /// The UIDs. + /// The message summary items to fetch. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task> FetchAsync (this IMailFolder folder, IList uids, MessageSummaryItems items, CancellationToken cancellationToken = default) + { + var request = new FetchRequest (items); + + return folder.FetchAsync (uids, request, cancellationToken); + } + + /// + /// Fetch the message summaries for the specified message UIDs. + /// + /// + /// Fetches the message summaries for the specified message UIDs. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The folder. + /// The UIDs. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// -or- + /// One or more of the specified is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static IList Fetch (this IMailFolder folder, IList uids, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default) + { + var request = new FetchRequest (items, headers); + + return folder.Fetch (uids, request, cancellationToken); + } + + /// + /// Asynchronously fetch the message summaries for the specified message UIDs. + /// + /// + /// Asynchronously fetches the message summaries for the specified message + /// UIDs. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The folder. + /// The UIDs. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// -or- + /// One or more of the specified is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task> FetchAsync (this IMailFolder folder, IList uids, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default) + { + var request = new FetchRequest (items, headers); + + return folder.FetchAsync (uids, request, cancellationToken); + } + + /// + /// Fetch the message summaries for the specified message UIDs. + /// + /// + /// Fetches the message summaries for the specified message UIDs. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The folder. + /// The UIDs. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// -or- + /// One or more of the specified is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static IList Fetch (this IMailFolder folder, IList uids, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default) + { + var request = new FetchRequest (items, headers); + + return folder.Fetch (uids, request, cancellationToken); + } + + /// + /// Asynchronously fetch the message summaries for the specified message UIDs. + /// + /// + /// Asynchronously fetches the message summaries for the specified message + /// UIDs. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The folder. + /// The UIDs. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// -or- + /// One or more of the specified is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task> FetchAsync (this IMailFolder folder, IList uids, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default) + { + var request = new FetchRequest (items, headers); + + return folder.FetchAsync (uids, request, cancellationToken); + } + + /// + /// Fetch the message summaries for the specified message UIDs that have a + /// higher mod-sequence value than the one specified. + /// + /// + /// Fetches the message summaries for the specified message UIDs that + /// have a higher mod-sequence value than the one specified. + /// If the mail store supports quick resynchronization and the application has + /// enabled this feature via , + /// then this method will emit events for messages that + /// have vanished since the specified mod-sequence value. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The folder. + /// The UIDs. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static IList Fetch (this IMailFolder folder, IList uids, ulong modseq, MessageSummaryItems items, CancellationToken cancellationToken = default) + { + var request = new FetchRequest (items) { ChangedSince = modseq }; + + return folder.Fetch (uids, request, cancellationToken); + } + + /// + /// Asynchronously fetch the message summaries for the specified message UIDs that have a + /// higher mod-sequence value than the one specified. + /// + /// + /// Asynchronously fetches the message summaries for the specified message UIDs that + /// have a higher mod-sequence value than the one specified. + /// If the mail store supports quick resynchronization and the application has + /// enabled this feature via , + /// then this method will emit events for messages that + /// have vanished since the specified mod-sequence value. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The folder. + /// The UIDs. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task> FetchAsync (this IMailFolder folder, IList uids, ulong modseq, MessageSummaryItems items, CancellationToken cancellationToken = default) + { + var request = new FetchRequest (items) { ChangedSince = modseq }; + + return folder.FetchAsync (uids, request, cancellationToken); + } + + /// + /// Fetch the message summaries for the specified message UIDs that have a + /// higher mod-sequence value than the one specified. + /// + /// + /// Fetches the message summaries for the specified message UIDs that + /// have a higher mod-sequence value than the one specified. + /// If the mail store supports quick resynchronization and the application has + /// enabled this feature via , + /// then this method will emit events for messages that + /// have vanished since the specified mod-sequence value. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The folder. + /// The UIDs. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// -or- + /// One or more of the specified is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static IList Fetch (this IMailFolder folder, IList uids, ulong modseq, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default) + { + var request = new FetchRequest (items, headers) { ChangedSince = modseq }; + + return folder.Fetch (uids, request, cancellationToken); + } + + /// + /// Asynchronously fetch the message summaries for the specified message UIDs that have a + /// higher mod-sequence value than the one specified. + /// + /// + /// Asynchronously fetches the message summaries for the specified message UIDs that + /// have a higher mod-sequence value than the one specified. + /// If the mail store supports quick resynchronization and the application has + /// enabled this feature via , + /// then this method will emit events for messages that + /// have vanished since the specified mod-sequence value. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The folder. + /// The UIDs. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// -or- + /// One or more of the specified is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task> FetchAsync (this IMailFolder folder, IList uids, ulong modseq, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default) + { + var request = new FetchRequest (items, headers) { ChangedSince = modseq }; + + return folder.FetchAsync (uids, request, cancellationToken); + } + + /// + /// Fetch the message summaries for the specified message UIDs that have a + /// higher mod-sequence value than the one specified. + /// + /// + /// Fetches the message summaries for the specified message UIDs that + /// have a higher mod-sequence value than the one specified. + /// If the mail store supports quick resynchronization and the application has + /// enabled this feature via , + /// then this method will emit events for messages that + /// have vanished since the specified mod-sequence value. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The folder. + /// The UIDs. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// -or- + /// One or more of the specified is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static IList Fetch (this IMailFolder folder, IList uids, ulong modseq, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default) + { + var request = new FetchRequest (items, headers) { ChangedSince = modseq }; + + return folder.Fetch (uids, request, cancellationToken); + } + + /// + /// Asynchronously fetch the message summaries for the specified message UIDs that have a + /// higher mod-sequence value than the one specified. + /// + /// + /// Asynchronously fetches the message summaries for the specified message UIDs that + /// have a higher mod-sequence value than the one specified. + /// If the mail store supports quick resynchronization and the application has + /// enabled this feature via , + /// then this method will emit events for messages that + /// have vanished since the specified mod-sequence value. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The folder. + /// The UIDs. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// -or- + /// One or more of the specified is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task> FetchAsync (this IMailFolder folder, IList uids, ulong modseq, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default) + { + var request = new FetchRequest (items, headers) { ChangedSince = modseq }; + + return folder.FetchAsync (uids, request, cancellationToken); + } + + /// + /// Fetch the message summaries for the specified message indexes. + /// + /// + /// Fetches the message summaries for the specified message indexes. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The folder. + /// The indexes. + /// The message summary items to fetch. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static IList Fetch (this IMailFolder folder, IList indexes, MessageSummaryItems items, CancellationToken cancellationToken = default) + { + var request = new FetchRequest (items); + + return folder.Fetch (indexes, request, cancellationToken); + } + + /// + /// Asynchronously fetch the message summaries for the specified message indexes. + /// + /// + /// Asynchronously fetches the message summaries for the specified message + /// indexes. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The folder. + /// The indexes. + /// The message summary items to fetch. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task> FetchAsync (this IMailFolder folder, IList indexes, MessageSummaryItems items, CancellationToken cancellationToken = default) + { + var request = new FetchRequest (items); + + return folder.FetchAsync (indexes, request, cancellationToken); + } + + /// + /// Fetch the message summaries for the specified message indexes. + /// + /// + /// Fetches the message summaries for the specified message indexes. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The folder. + /// The indexes. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// -or- + /// One or more of the specified is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static IList Fetch (this IMailFolder folder, IList indexes, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default) + { + var request = new FetchRequest (items, headers); + + return folder.Fetch (indexes, request, cancellationToken); + } + + /// + /// Asynchronously fetch the message summaries for the specified message indexes. + /// + /// + /// Asynchronously fetches the message summaries for the specified message + /// indexes. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The folder. + /// The indexes. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// -or- + /// One or more of the specified is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task> FetchAsync (this IMailFolder folder, IList indexes, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default) + { + var request = new FetchRequest (items, headers); + + return folder.FetchAsync (indexes, request, cancellationToken); + } + + /// + /// Fetch the message summaries for the specified message indexes. + /// + /// + /// Fetches the message summaries for the specified message indexes. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The folder. + /// The indexes. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// -or- + /// One or more of the specified is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static IList Fetch (this IMailFolder folder, IList indexes, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default) + { + var request = new FetchRequest (items, headers); + + return folder.Fetch (indexes, request, cancellationToken); + } + + /// + /// Asynchronously fetch the message summaries for the specified message indexes. + /// + /// + /// Asynchronously fetches the message summaries for the specified message + /// indexes. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The folder. + /// The indexes. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// -or- + /// One or more of the specified is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task> FetchAsync (this IMailFolder folder, IList indexes, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default) + { + var request = new FetchRequest (items, headers); + + return folder.FetchAsync (indexes, request, cancellationToken); + } + + /// + /// Fetch the message summaries for the specified message indexes that have a + /// higher mod-sequence value than the one specified. + /// + /// + /// Fetches the message summaries for the specified message indexes that + /// have a higher mod-sequence value than the one specified. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The folder. + /// The indexes. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static IList Fetch (this IMailFolder folder, IList indexes, ulong modseq, MessageSummaryItems items, CancellationToken cancellationToken = default) + { + var request = new FetchRequest (items) { ChangedSince = modseq }; + + return folder.Fetch (indexes, request, cancellationToken); + } + + /// + /// Asynchronously fetch the message summaries for the specified message indexes that have a + /// higher mod-sequence value than the one specified. + /// + /// + /// Asynchronously fetches the message summaries for the specified message + /// indexes that have a higher mod-sequence value than the one specified. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The folder. + /// The indexes. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task> FetchAsync (this IMailFolder folder, IList indexes, ulong modseq, MessageSummaryItems items, CancellationToken cancellationToken = default) + { + var request = new FetchRequest (items) { ChangedSince = modseq }; + + return folder.FetchAsync (indexes, request, cancellationToken); + } + + /// + /// Fetch the message summaries for the specified message indexes that have a + /// higher mod-sequence value than the one specified. + /// + /// + /// Fetches the message summaries for the specified message indexes that + /// have a higher mod-sequence value than the one specified. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The folder. + /// The indexes. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// -or- + /// One or more of the specified is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static IList Fetch (this IMailFolder folder, IList indexes, ulong modseq, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default) + { + var request = new FetchRequest (items, headers) { ChangedSince = modseq }; + + return folder.Fetch (indexes, request, cancellationToken); + } + + /// + /// Asynchronously fetch the message summaries for the specified message indexes + /// that have a higher mod-sequence value than the one specified. + /// + /// + /// Asynchronously fetches the message summaries for the specified message + /// indexes that have a higher mod-sequence value than the one specified. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The folder. + /// The indexes. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// -or- + /// One or more of the specified is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task> FetchAsync (this IMailFolder folder, IList indexes, ulong modseq, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default) + { + var request = new FetchRequest (items, headers) { ChangedSince = modseq }; + + return folder.FetchAsync (indexes, request, cancellationToken); + } + + /// + /// Fetch the message summaries for the specified message indexes that + /// have a higher mod-sequence value than the one specified. + /// + /// + /// Fetches the message summaries for the specified message indexes that + /// have a higher mod-sequence value than the one specified. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The folder. + /// The indexes. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// -or- + /// One or more of the specified is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static IList Fetch (this IMailFolder folder, IList indexes, ulong modseq, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default) + { + var request = new FetchRequest (items, headers) { ChangedSince = modseq }; + + return folder.Fetch (indexes, request, cancellationToken); + } + + /// + /// Asynchronously fetch the message summaries for the specified message indexes + /// that have a higher mod-sequence value than the one specified. + /// + /// + /// Asynchronously fetches the message summaries for the specified message + /// indexes that have a higher mod-sequence value than the one specified. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The folder. + /// The indexes. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// -or- + /// One or more of the specified is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task> FetchAsync (this IMailFolder folder, IList indexes, ulong modseq, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default) + { + var request = new FetchRequest (items, headers) { ChangedSince = modseq }; + + return folder.FetchAsync (indexes, request, cancellationToken); + } + + /// + /// Fetch the message summaries for the messages between the two indexes, inclusive. + /// + /// + /// Fetches the message summaries for the messages between the two + /// indexes, inclusive. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The folder. + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// The message summary items to fetch. + /// The cancellation token. + /// + /// is out of range. + /// -or- + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static IList Fetch (this IMailFolder folder, int min, int max, MessageSummaryItems items, CancellationToken cancellationToken = default) + { + var request = new FetchRequest (items); + + return folder.Fetch (min, max, request, cancellationToken); + } + + /// + /// Asynchronously fetch the message summaries for the messages between the two indexes, inclusive. + /// + /// + /// Asynchronously fetches the message summaries for the messages between + /// the two indexes, inclusive. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The folder. + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// The message summary items to fetch. + /// The cancellation token. + /// + /// is out of range. + /// -or- + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task> FetchAsync (this IMailFolder folder, int min, int max, MessageSummaryItems items, CancellationToken cancellationToken = default) + { + var request = new FetchRequest (items); + + return folder.FetchAsync (min, max, request, cancellationToken); + } + + /// + /// Fetch the message summaries for the messages between the two indexes, inclusive. + /// + /// + /// Fetches the message summaries for the messages between the two + /// indexes, inclusive. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The folder. + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is out of range. + /// -or- + /// is out of range. + /// + /// + /// is . + /// + /// + /// One or more of the specified is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static IList Fetch (this IMailFolder folder, int min, int max, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default) + { + var request = new FetchRequest (items, headers); + + return folder.Fetch (min, max, request, cancellationToken); + } + + /// + /// Asynchronously fetch the message summaries for the messages between the two indexes, inclusive. + /// + /// + /// Asynchronously fetches the message summaries for the messages between + /// the two indexes, inclusive. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The folder. + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is out of range. + /// -or- + /// is out of range. + /// + /// + /// is . + /// + /// + /// One or more of the specified is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task> FetchAsync (this IMailFolder folder, int min, int max, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default) + { + var request = new FetchRequest (items, headers); + + return folder.FetchAsync (min, max, request, cancellationToken); + } + + /// + /// Fetch the message summaries for the messages between the two indexes, inclusive. + /// + /// + /// Fetches the message summaries for the messages between the two + /// indexes, inclusive. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The folder. + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is out of range. + /// -or- + /// is out of range. + /// + /// + /// is . + /// + /// + /// One or more of the specified is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static IList Fetch (this IMailFolder folder, int min, int max, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default) + { + var request = new FetchRequest (items, headers); + + return folder.Fetch (min, max, request, cancellationToken); + } + + /// + /// Asynchronously fetch the message summaries for the messages between the two indexes, inclusive. + /// + /// + /// Asynchronously fetches the message summaries for the messages between + /// the two indexes, inclusive. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The folder. + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is out of range. + /// -or- + /// is out of range. + /// + /// + /// is . + /// + /// + /// One or more of the specified is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task> FetchAsync (this IMailFolder folder, int min, int max, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default) + { + var request = new FetchRequest (items, headers); + + return folder.FetchAsync (min, max, request, cancellationToken); + } + + /// + /// Fetch the message summaries for the messages between the two indexes (inclusive) + /// that have a higher mod-sequence value than the one specified. + /// + /// + /// Fetches the message summaries for the messages between the two + /// indexes (inclusive) that have a higher mod-sequence value than the one + /// specified. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The folder. + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The cancellation token. + /// + /// is out of range. + /// -or- + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static IList Fetch (this IMailFolder folder, int min, int max, ulong modseq, MessageSummaryItems items, CancellationToken cancellationToken = default) + { + var request = new FetchRequest (items) { ChangedSince = modseq }; + + return folder.Fetch (min, max, request, cancellationToken); + } + + /// + /// Asynchronously fetch the message summaries for the messages between the two indexes + /// (inclusive) that have a higher mod-sequence value than the one specified. + /// + /// + /// Asynchronously fetches the message summaries for the messages between + /// the two indexes (inclusive) that have a higher mod-sequence value than the + /// one specified. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The folder. + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The cancellation token. + /// + /// is out of range. + /// -or- + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task> FetchAsync (this IMailFolder folder, int min, int max, ulong modseq, MessageSummaryItems items, CancellationToken cancellationToken = default) + { + var request = new FetchRequest (items) { ChangedSince = modseq }; + + return folder.FetchAsync (min, max, request, cancellationToken); + } + + /// + /// Fetch the message summaries for the messages between the two indexes (inclusive) + /// that have a higher mod-sequence value than the one specified. + /// + /// + /// Fetches the message summaries for the messages between the two + /// indexes (inclusive) that have a higher mod-sequence value than the one + /// specified. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The folder. + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is out of range. + /// -or- + /// is out of range. + /// + /// + /// is . + /// + /// + /// One or more of the specified is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static IList Fetch (this IMailFolder folder, int min, int max, ulong modseq, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default) + { + var request = new FetchRequest (items, headers) { ChangedSince = modseq }; + + return folder.Fetch (min, max, request, cancellationToken); + } + + /// + /// Asynchronously fetch the message summaries for the messages between the two indexes + /// (inclusive) that have a higher mod-sequence value than the one specified. + /// + /// + /// Asynchronously fetches the message summaries for the messages between + /// the two indexes (inclusive) that have a higher mod-sequence value than the + /// one specified. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The folder. + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is out of range. + /// -or- + /// is out of range. + /// + /// + /// is . + /// + /// + /// One or more of the specified is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task> FetchAsync (this IMailFolder folder, int min, int max, ulong modseq, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default) + { + var request = new FetchRequest (items, headers) { ChangedSince = modseq }; + + return folder.FetchAsync (min, max, request, cancellationToken); + } + + /// + /// Fetch the message summaries for the messages between the two indexes (inclusive) + /// that have a higher mod-sequence value than the one specified. + /// + /// + /// Fetches the message summaries for the messages between the two + /// indexes (inclusive) that have a higher mod-sequence value than the one + /// specified. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The folder. + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is out of range. + /// -or- + /// is out of range. + /// + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static IList Fetch (this IMailFolder folder, int min, int max, ulong modseq, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default) + { + var request = new FetchRequest (items, headers) { ChangedSince = modseq }; + + return folder.Fetch (min, max, request, cancellationToken); + } + + /// + /// Asynchronously fetch the message summaries for the messages between the two indexes + /// (inclusive) that have a higher mod-sequence value than the one specified. + /// + /// + /// Asynchronously fetches the message summaries for the messages between + /// the two indexes (inclusive) that have a higher mod-sequence value than the + /// one specified. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The folder. + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is out of range. + /// -or- + /// is out of range. + /// + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task> FetchAsync (this IMailFolder folder, int min, int max, ulong modseq, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default) + { + var request = new FetchRequest (items, headers) { ChangedSince = modseq }; + + return folder.FetchAsync (min, max, request, cancellationToken); + } + } +} diff --git a/MailKit/IMailFolderStoreExtensions.cs b/MailKit/IMailFolderStoreExtensions.cs new file mode 100644 index 0000000000..f25522cf38 --- /dev/null +++ b/MailKit/IMailFolderStoreExtensions.cs @@ -0,0 +1,5316 @@ +// +// IMailFolderStoreExtensions.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; + +namespace MailKit { + public static partial class IMailFolderExtensions + { + #region Store Flags Extensions + + static StoreFlagsRequest GetStoreFlagsRequest (StoreAction action, bool silent, MessageFlags flags, HashSet? keywords = null, ulong? modseq = null) + { + if (keywords != null) { + return new StoreFlagsRequest (action, flags, keywords) { + UnchangedSince = modseq, + Silent = silent + }; + } else { + return new StoreFlagsRequest (action, flags) { + UnchangedSince = modseq, + Silent = silent + }; + } + } + + /// + /// Add a set of flags to the specified message. + /// + /// + /// Adds a set of flags to the specified message. + /// + /// The folder. + /// The UID of the message. + /// The message flags to add. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static void AddFlags (this IMailFolder folder, UniqueId uid, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) + { + folder.Store (uid, GetStoreFlagsRequest (StoreAction.Add, silent, flags), cancellationToken); + } + + /// + /// Asynchronously add a set of flags to the specified message. + /// + /// + /// Asynchronously adds a set of flags to the specified message. + /// + /// An asynchronous task context. + /// The folder. + /// The UID of the message. + /// The message flags to add. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task AddFlagsAsync (this IMailFolder folder, UniqueId uid, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) + { + return folder.StoreAsync (uid, GetStoreFlagsRequest (StoreAction.Add, silent, flags), cancellationToken); + } + + /// + /// Add a set of flags to the specified message. + /// + /// + /// Adds a set of flags to the specified message. + /// + /// The folder. + /// The UID of the message. + /// The message flags to add. + /// A set of user-defined flags to add. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static void AddFlags (this IMailFolder folder, UniqueId uid, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default) + { + folder.Store (uid, GetStoreFlagsRequest (StoreAction.Add, silent, flags, keywords), cancellationToken); + } + + /// + /// Asynchronously add a set of flags to the specified message. + /// + /// + /// Asynchronously adds a set of flags to the specified message. + /// + /// An asynchronous task context. + /// The folder. + /// The UID of the message. + /// The message flags to add. + /// A set of user-defined flags to add. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task AddFlagsAsync (this IMailFolder folder, UniqueId uid, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default) + { + return folder.StoreAsync (uid, GetStoreFlagsRequest (StoreAction.Add, silent, flags, keywords), cancellationToken); + } + + /// + /// Add a set of flags to the specified messages. + /// + /// + /// Adds a set of flags to the specified messages. + /// + /// The folder. + /// The UIDs of the messages. + /// The message flags to add. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static void AddFlags (this IMailFolder folder, IList uids, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) + { + folder.Store (uids, GetStoreFlagsRequest (StoreAction.Add, silent, flags), cancellationToken); + } + + /// + /// Asynchronously add a set of flags to the specified messages. + /// + /// + /// Asynchronously adds a set of flags to the specified messages. + /// + /// An asynchronous task context. + /// The folder. + /// The UIDs of the messages. + /// The message flags to add. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task AddFlagsAsync (this IMailFolder folder, IList uids, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) + { + return folder.StoreAsync (uids, GetStoreFlagsRequest (StoreAction.Add, silent, flags), cancellationToken); + } + + /// + /// Add a set of flags to the specified messages. + /// + /// + /// Adds a set of flags to the specified messages. + /// + /// The folder. + /// The UIDs of the messages. + /// The message flags to add. + /// A set of user-defined flags to add. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static void AddFlags (this IMailFolder folder, IList uids, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default) + { + folder.Store (uids, GetStoreFlagsRequest (StoreAction.Add, silent, flags, keywords), cancellationToken); + } + + /// + /// Asynchronously add a set of flags to the specified messages. + /// + /// + /// Asynchronously adds a set of flags to the specified messages. + /// + /// An asynchronous task context. + /// The folder. + /// The UIDs of the messages. + /// The message flags to add. + /// A set of user-defined flags to add. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task AddFlagsAsync (this IMailFolder folder, IList uids, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default) + { + return folder.StoreAsync (uids, GetStoreFlagsRequest (StoreAction.Add, silent, flags, keywords), cancellationToken); + } + + /// + /// Remove a set of flags from the specified message. + /// + /// + /// Removes a set of flags from the specified message. + /// + /// The folder. + /// The UIDs of the message. + /// The message flags to remove. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static void RemoveFlags (this IMailFolder folder, UniqueId uid, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) + { + folder.Store (uid, GetStoreFlagsRequest (StoreAction.Remove, silent, flags), cancellationToken); + } + + /// + /// Asynchronously remove a set of flags from the specified message. + /// + /// + /// Asynchronously removes a set of flags from the specified message. + /// + /// An asynchronous task context. + /// The folder. + /// The UID of the message. + /// The message flags to remove. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task RemoveFlagsAsync (this IMailFolder folder, UniqueId uid, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) + { + return folder.StoreAsync (uid, GetStoreFlagsRequest (StoreAction.Remove, silent, flags), cancellationToken); + } + + /// + /// Remove a set of flags from the specified message. + /// + /// + /// Removes a set of flags from the specified message. + /// + /// The folder. + /// The UIDs of the message. + /// The message flags to remove. + /// A set of user-defined flags to remove. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static void RemoveFlags (this IMailFolder folder, UniqueId uid, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default) + { + folder.Store (uid, GetStoreFlagsRequest (StoreAction.Remove, silent, flags, keywords), cancellationToken); + } + + /// + /// Asynchronously remove a set of flags from the specified message. + /// + /// + /// Asynchronously removes a set of flags from the specified message. + /// + /// An asynchronous task context. + /// The folder. + /// The UID of the message. + /// The message flags to remove. + /// A set of user-defined flags to remove. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task RemoveFlagsAsync (this IMailFolder folder, UniqueId uid, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default) + { + return folder.StoreAsync (uid, GetStoreFlagsRequest (StoreAction.Remove, silent, flags, keywords), cancellationToken); + } + + /// + /// Remove a set of flags from the specified messages. + /// + /// + /// Removes a set of flags from the specified messages. + /// + /// The folder. + /// The UIDs of the messages. + /// The message flags to remove. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static void RemoveFlags (this IMailFolder folder, IList uids, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) + { + folder.Store (uids, GetStoreFlagsRequest (StoreAction.Remove, silent, flags), cancellationToken); + } + + /// + /// Asynchronously remove a set of flags from the specified messages. + /// + /// + /// Asynchronously removes a set of flags from the specified messages. + /// + /// An asynchronous task context. + /// The folder. + /// The UIDs of the messages. + /// The message flags to remove. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task RemoveFlagsAsync (this IMailFolder folder, IList uids, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) + { + return folder.StoreAsync (uids, GetStoreFlagsRequest (StoreAction.Remove, silent, flags), cancellationToken); + } + + /// + /// Remove a set of flags from the specified messages. + /// + /// + /// Removes a set of flags from the specified messages. + /// + /// The folder. + /// The UIDs of the messages. + /// The message flags to remove. + /// A set of user-defined flags to remove. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static void RemoveFlags (this IMailFolder folder, IList uids, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default) + { + folder.Store (uids, GetStoreFlagsRequest (StoreAction.Remove, silent, flags, keywords), cancellationToken); + } + + /// + /// Asynchronously remove a set of flags from the specified messages. + /// + /// + /// Asynchronously removes a set of flags from the specified messages. + /// + /// An asynchronous task context. + /// The folder. + /// The UIDs of the messages. + /// The message flags to remove. + /// A set of user-defined flags to remove. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task RemoveFlagsAsync (this IMailFolder folder, IList uids, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default) + { + return folder.StoreAsync (uids, GetStoreFlagsRequest (StoreAction.Remove, silent, flags, keywords), cancellationToken); + } + + /// + /// Set the flags of the specified message. + /// + /// + /// Sets the flags of the specified message. + /// + /// The folder. + /// The UIDs of the message. + /// The message flags to set. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static void SetFlags (this IMailFolder folder, UniqueId uid, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) + { + folder.Store (uid, GetStoreFlagsRequest (StoreAction.Set, silent, flags), cancellationToken); + } + + /// + /// Asynchronously set the flags of the specified message. + /// + /// + /// Asynchronously sets the flags of the specified message. + /// + /// An asynchronous task context. + /// The folder. + /// The UID of the message. + /// The message flags to set. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task SetFlagsAsync (this IMailFolder folder, UniqueId uid, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) + { + return folder.StoreAsync (uid, GetStoreFlagsRequest (StoreAction.Set, silent, flags), cancellationToken); + } + + /// + /// Set the flags of the specified message. + /// + /// + /// Sets the flags of the specified message. + /// + /// The folder. + /// The UIDs of the message. + /// The message flags to set. + /// A set of user-defined flags to set. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static void SetFlags (this IMailFolder folder, UniqueId uid, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default) + { + folder.Store (uid, GetStoreFlagsRequest (StoreAction.Set, silent, flags, keywords), cancellationToken); + } + + /// + /// Asynchronously set the flags of the specified message. + /// + /// + /// Asynchronously sets the flags of the specified message. + /// + /// An asynchronous task context. + /// The folder. + /// The UID of the message. + /// The message flags to set. + /// A set of user-defined flags to set. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task SetFlagsAsync (this IMailFolder folder, UniqueId uid, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default) + { + return folder.StoreAsync (uid, GetStoreFlagsRequest (StoreAction.Set, silent, flags, keywords), cancellationToken); + } + + /// + /// Set the flags of the specified messages. + /// + /// + /// Sets the flags of the specified messages. + /// + /// The folder. + /// The UIDs of the messages. + /// The message flags to set. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static void SetFlags (this IMailFolder folder, IList uids, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) + { + folder.Store (uids, GetStoreFlagsRequest (StoreAction.Set, silent, flags), cancellationToken); + } + + /// + /// Asynchronously set the flags of the specified messages. + /// + /// + /// Asynchronously sets the flags of the specified messages. + /// + /// An asynchronous task context. + /// The folder. + /// The UIDs of the messages. + /// The message flags to set. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task SetFlagsAsync (this IMailFolder folder, IList uids, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) + { + return folder.StoreAsync (uids, GetStoreFlagsRequest (StoreAction.Set, silent, flags), cancellationToken); + } + + /// + /// Set the flags of the specified messages. + /// + /// + /// Sets the flags of the specified messages. + /// + /// The folder. + /// The UIDs of the messages. + /// The message flags to set. + /// A set of user-defined flags to set. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static void SetFlags (this IMailFolder folder, IList uids, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default) + { + folder.Store (uids, GetStoreFlagsRequest (StoreAction.Set, silent, flags, keywords), cancellationToken); + } + + /// + /// Asynchronously set the flags of the specified messages. + /// + /// + /// Asynchronously sets the flags of the specified messages. + /// + /// An asynchronous task context. + /// The folder. + /// The UIDs of the messages. + /// The message flags to set. + /// A set of user-defined flags to set. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task SetFlagsAsync (this IMailFolder folder, IList uids, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default) + { + return folder.StoreAsync (uids, GetStoreFlagsRequest (StoreAction.Set, silent, flags, keywords), cancellationToken); + } + + /// + /// Add a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Adds a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The folder. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The message flags to add. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static IList AddFlags (this IMailFolder folder, IList uids, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) + { + return folder.Store (uids, GetStoreFlagsRequest (StoreAction.Add, silent, flags, null, modseq), cancellationToken); + } + + /// + /// Asynchronously add a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously adds a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The folder. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The message flags to add. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task> AddFlagsAsync (this IMailFolder folder, IList uids, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) + { + return folder.StoreAsync (uids, GetStoreFlagsRequest (StoreAction.Add, silent, flags, null, modseq), cancellationToken); + } + + /// + /// Add a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Adds a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The folder. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The message flags to add. + /// A set of user-defined flags to add. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static IList AddFlags (this IMailFolder folder, IList uids, ulong modseq, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default) + { + return folder.Store (uids, GetStoreFlagsRequest (StoreAction.Add, silent, flags, keywords, modseq), cancellationToken); + } + + /// + /// Asynchronously add a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously adds a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The folder. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The message flags to add. + /// A set of user-defined flags to add. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task> AddFlagsAsync (this IMailFolder folder, IList uids, ulong modseq, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default) + { + return folder.StoreAsync (uids, GetStoreFlagsRequest (StoreAction.Add, silent, flags, keywords, modseq), cancellationToken); + } + + /// + /// Remove a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Removes a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The folder. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The message flags to remove. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static IList RemoveFlags (this IMailFolder folder, IList uids, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) + { + return folder.Store (uids, GetStoreFlagsRequest (StoreAction.Remove, silent, flags, null, modseq), cancellationToken); + } + + /// + /// Asynchronously remove a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously removes a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The folder. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The message flags to remove. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task> RemoveFlagsAsync (this IMailFolder folder, IList uids, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) + { + return folder.StoreAsync (uids, GetStoreFlagsRequest (StoreAction.Remove, silent, flags, null, modseq), cancellationToken); + } + + /// + /// Remove a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Removes a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The folder. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The message flags to remove. + /// A set of user-defined flags to remove. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static IList RemoveFlags (this IMailFolder folder, IList uids, ulong modseq, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default) + { + return folder.Store (uids, GetStoreFlagsRequest (StoreAction.Remove, silent, flags, keywords, modseq), cancellationToken); + } + + /// + /// Asynchronously remove a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously removes a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The folder. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The message flags to remove. + /// A set of user-defined flags to remove. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task> RemoveFlagsAsync (this IMailFolder folder, IList uids, ulong modseq, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default) + { + return folder.StoreAsync (uids, GetStoreFlagsRequest (StoreAction.Remove, silent, flags, keywords, modseq), cancellationToken); + } + + /// + /// Set the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Sets the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The folder. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The message flags to set. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static IList SetFlags (this IMailFolder folder, IList uids, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) + { + return folder.Store (uids, GetStoreFlagsRequest (StoreAction.Set, silent, flags, null, modseq), cancellationToken); + } + + /// + /// Asynchronously set the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously sets the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The folder. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The message flags to set. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task> SetFlagsAsync (this IMailFolder folder, IList uids, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) + { + return folder.StoreAsync (uids, GetStoreFlagsRequest (StoreAction.Set, silent, flags, null, modseq), cancellationToken); + } + + /// + /// Set the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Sets the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The folder. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The message flags to set. + /// A set of user-defined flags to set. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static IList SetFlags (this IMailFolder folder, IList uids, ulong modseq, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default) + { + return folder.Store (uids, GetStoreFlagsRequest (StoreAction.Set, silent, flags, keywords, modseq), cancellationToken); + } + + /// + /// Asynchronously set the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously sets the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The folder. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The message flags to set. + /// A set of user-defined flags to set. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task> SetFlagsAsync (this IMailFolder folder, IList uids, ulong modseq, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default) + { + return folder.StoreAsync (uids, GetStoreFlagsRequest (StoreAction.Set, silent, flags, keywords, modseq), cancellationToken); + } + + /// + /// Add a set of flags to the specified message. + /// + /// + /// Adds a set of flags to the specified message. + /// + /// The folder. + /// The index of the message. + /// The message flags to add. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static void AddFlags (this IMailFolder folder, int index, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) + { + folder.Store (index, GetStoreFlagsRequest (StoreAction.Add, silent, flags), cancellationToken); + } + + /// + /// Asynchronously add a set of flags to the specified message. + /// + /// + /// Asynchronously adds a set of flags to the specified message. + /// + /// An asynchronous task context. + /// The folder. + /// The index of the messages. + /// The message flags to add. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task AddFlagsAsync (this IMailFolder folder, int index, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) + { + return folder.StoreAsync (index, GetStoreFlagsRequest (StoreAction.Add, silent, flags), cancellationToken); + } + + /// + /// Add a set of flags to the specified message. + /// + /// + /// Adds a set of flags to the specified message. + /// + /// The folder. + /// The index of the message. + /// The message flags to add. + /// A set of user-defined flags to add. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static void AddFlags (this IMailFolder folder, int index, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default) + { + folder.Store (index, GetStoreFlagsRequest (StoreAction.Add, silent, flags, keywords), cancellationToken); + } + + /// + /// Asynchronously add a set of flags to the specified message. + /// + /// + /// Asynchronously adds a set of flags to the specified message. + /// + /// An asynchronous task context. + /// The folder. + /// The index of the messages. + /// The message flags to add. + /// A set of user-defined flags to add. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task AddFlagsAsync (this IMailFolder folder, int index, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default) + { + return folder.StoreAsync (index, GetStoreFlagsRequest (StoreAction.Add, silent, flags, keywords), cancellationToken); + } + + /// + /// Add a set of flags to the specified messages. + /// + /// + /// Adds a set of flags to the specified messages. + /// + /// The folder. + /// The indexes of the messages. + /// The message flags to add. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static void AddFlags (this IMailFolder folder, IList indexes, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) + { + folder.Store (indexes, GetStoreFlagsRequest (StoreAction.Add, silent, flags), cancellationToken); + } + + /// + /// Asynchronously add a set of flags to the specified messages. + /// + /// + /// Asynchronously adds a set of flags to the specified messages. + /// + /// An asynchronous task context. + /// The folder. + /// The indexes of the messages. + /// The message flags to add. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task AddFlagsAsync (this IMailFolder folder, IList indexes, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) + { + return folder.StoreAsync (indexes, GetStoreFlagsRequest (StoreAction.Add, silent, flags), cancellationToken); + } + + /// + /// Add a set of flags to the specified messages. + /// + /// + /// Adds a set of flags to the specified messages. + /// + /// The folder. + /// The indexes of the messages. + /// The message flags to add. + /// A set of user-defined flags to add. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static void AddFlags (this IMailFolder folder, IList indexes, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default) + { + folder.Store (indexes, GetStoreFlagsRequest (StoreAction.Add, silent, flags, keywords), cancellationToken); + } + + /// + /// Asynchronously add a set of flags to the specified messages. + /// + /// + /// Asynchronously adds a set of flags to the specified messages. + /// + /// An asynchronous task context. + /// The folder. + /// The indexes of the messages. + /// The message flags to add. + /// A set of user-defined flags to add. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task AddFlagsAsync (this IMailFolder folder, IList indexes, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default) + { + return folder.StoreAsync (indexes, GetStoreFlagsRequest (StoreAction.Add, silent, flags, keywords), cancellationToken); + } + + /// + /// Remove a set of flags from the specified message. + /// + /// + /// Removes a set of flags from the specified message. + /// + /// The folder. + /// The index of the message. + /// The message flags to remove. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static void RemoveFlags (this IMailFolder folder, int index, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) + { + folder.Store (index, GetStoreFlagsRequest (StoreAction.Remove, silent, flags), cancellationToken); + } + + /// + /// Asynchronously remove a set of flags from the specified message. + /// + /// + /// Asynchronously removes a set of flags from the specified message. + /// + /// An asynchronous task context. + /// The folder. + /// The index of the message. + /// The message flags to remove. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task RemoveFlagsAsync (this IMailFolder folder, int index, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) + { + return folder.StoreAsync (index, GetStoreFlagsRequest (StoreAction.Remove, silent, flags), cancellationToken); + } + + /// + /// Remove a set of flags from the specified message. + /// + /// + /// Removes a set of flags from the specified message. + /// + /// The folder. + /// The index of the message. + /// The message flags to remove. + /// A set of user-defined flags to remove. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static void RemoveFlags (this IMailFolder folder, int index, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default) + { + folder.Store (index, GetStoreFlagsRequest (StoreAction.Remove, silent, flags, keywords), cancellationToken); + } + + /// + /// Asynchronously remove a set of flags from the specified message. + /// + /// + /// Asynchronously removes a set of flags from the specified message. + /// + /// An asynchronous task context. + /// The folder. + /// The index of the message. + /// The message flags to remove. + /// A set of user-defined flags to remove. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task RemoveFlagsAsync (this IMailFolder folder, int index, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default) + { + return folder.StoreAsync (index, GetStoreFlagsRequest (StoreAction.Remove, silent, flags, keywords), cancellationToken); + } + + /// + /// Remove a set of flags from the specified messages. + /// + /// + /// Removes a set of flags from the specified messages. + /// + /// The folder. + /// The indexes of the messages. + /// The message flags to remove. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static void RemoveFlags (this IMailFolder folder, IList indexes, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) + { + folder.Store (indexes, GetStoreFlagsRequest (StoreAction.Remove, silent, flags), cancellationToken); + } + + /// + /// Asynchronously remove a set of flags from the specified messages. + /// + /// + /// Asynchronously removes a set of flags from the specified messages. + /// + /// An asynchronous task context. + /// The folder. + /// The indexes of the messages. + /// The message flags to remove. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task RemoveFlagsAsync (this IMailFolder folder, IList indexes, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) + { + return folder.StoreAsync (indexes, GetStoreFlagsRequest (StoreAction.Remove, silent, flags), cancellationToken); + } + + /// + /// Remove a set of flags from the specified messages. + /// + /// + /// Removes a set of flags from the specified messages. + /// + /// The folder. + /// The indexes of the messages. + /// The message flags to remove. + /// A set of user-defined flags to remove. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static void RemoveFlags (this IMailFolder folder, IList indexes, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default) + { + folder.Store (indexes, GetStoreFlagsRequest (StoreAction.Remove, silent, flags, keywords), cancellationToken); + } + + /// + /// Asynchronously remove a set of flags from the specified messages. + /// + /// + /// Asynchronously removes a set of flags from the specified messages. + /// + /// An asynchronous task context. + /// The folder. + /// The indexes of the messages. + /// The message flags to remove. + /// A set of user-defined flags to remove. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task RemoveFlagsAsync (this IMailFolder folder, IList indexes, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default) + { + return folder.StoreAsync (indexes, GetStoreFlagsRequest (StoreAction.Remove, silent, flags, keywords), cancellationToken); + } + + /// + /// Set the flags of the specified message. + /// + /// + /// Sets the flags of the specified message. + /// + /// The folder. + /// The index of the message. + /// The message flags to set. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static void SetFlags (this IMailFolder folder, int index, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) + { + folder.Store (index, GetStoreFlagsRequest (StoreAction.Set, silent, flags), cancellationToken); + } + + /// + /// Asynchronously set the flags of the specified message. + /// + /// + /// Asynchronously sets the flags of the specified message. + /// + /// An asynchronous task context. + /// The folder. + /// The index of the message. + /// The message flags to set. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task SetFlagsAsync (this IMailFolder folder, int index, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) + { + return folder.StoreAsync (index, GetStoreFlagsRequest (StoreAction.Set, silent, flags), cancellationToken); + } + + /// + /// Set the flags of the specified message. + /// + /// + /// Sets the flags of the specified message. + /// + /// The folder. + /// The index of the message. + /// The message flags to set. + /// A set of user-defined flags to set. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static void SetFlags (this IMailFolder folder, int index, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default) + { + folder.Store (index, GetStoreFlagsRequest (StoreAction.Set, silent, flags, keywords), cancellationToken); + } + + /// + /// Asynchronously set the flags of the specified message. + /// + /// + /// Asynchronously sets the flags of the specified message. + /// + /// An asynchronous task context. + /// The folder. + /// The index of the message. + /// The message flags to set. + /// A set of user-defined flags to set. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task SetFlagsAsync (this IMailFolder folder, int index, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default) + { + return folder.StoreAsync (index, GetStoreFlagsRequest (StoreAction.Set, silent, flags, keywords), cancellationToken); + } + + /// + /// Set the flags of the specified messages. + /// + /// + /// Sets the flags of the specified messages. + /// + /// The folder. + /// The indexes of the messages. + /// The message flags to set. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static void SetFlags (this IMailFolder folder, IList indexes, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) + { + folder.Store (indexes, GetStoreFlagsRequest (StoreAction.Set, silent, flags), cancellationToken); + } + + /// + /// Asynchronously set the flags of the specified messages. + /// + /// + /// Asynchronously sets the flags of the specified messages. + /// + /// An asynchronous task context. + /// The folder. + /// The indexes of the messages. + /// The message flags to set. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task SetFlagsAsync (this IMailFolder folder, IList indexes, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) + { + return folder.StoreAsync (indexes, GetStoreFlagsRequest (StoreAction.Set, silent, flags), cancellationToken); + } + + /// + /// Set the flags of the specified messages. + /// + /// + /// Sets the flags of the specified messages. + /// + /// The folder. + /// The indexes of the messages. + /// The message flags to set. + /// A set of user-defined flags to set. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static void SetFlags (this IMailFolder folder, IList indexes, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default) + { + folder.Store (indexes, GetStoreFlagsRequest (StoreAction.Set, silent, flags, keywords), cancellationToken); + } + + /// + /// Asynchronously set the flags of the specified messages. + /// + /// + /// Asynchronously sets the flags of the specified messages. + /// + /// An asynchronous task context. + /// The folder. + /// The indexes of the messages. + /// The message flags to set. + /// A set of user-defined flags to set. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task SetFlagsAsync (this IMailFolder folder, IList indexes, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default) + { + return folder.StoreAsync (indexes, GetStoreFlagsRequest (StoreAction.Set, silent, flags, keywords), cancellationToken); + } + + /// + /// Add a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Adds a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The folder. + /// The indexes of the messages. + /// The mod-sequence value. + /// The message flags to add. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static IList AddFlags (this IMailFolder folder, IList indexes, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) + { + return folder.Store (indexes, GetStoreFlagsRequest (StoreAction.Add, silent, flags, null, modseq), cancellationToken); + } + + /// + /// Asynchronously add a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously adds a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The folder. + /// The indexes of the messages. + /// The mod-sequence value. + /// The message flags to add. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task> AddFlagsAsync (this IMailFolder folder, IList indexes, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) + { + return folder.StoreAsync (indexes, GetStoreFlagsRequest (StoreAction.Add, silent, flags, null, modseq), cancellationToken); + } + + /// + /// Add a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Adds a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The folder. + /// The indexes of the messages. + /// The mod-sequence value. + /// The message flags to add. + /// A set of user-defined flags to add. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static IList AddFlags (this IMailFolder folder, IList indexes, ulong modseq, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default) + { + return folder.Store (indexes, GetStoreFlagsRequest (StoreAction.Add, silent, flags, keywords, modseq), cancellationToken); + } + + /// + /// Asynchronously add a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously adds a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The folder. + /// The indexes of the messages. + /// The mod-sequence value. + /// The message flags to add. + /// A set of user-defined flags to add. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task> AddFlagsAsync (this IMailFolder folder, IList indexes, ulong modseq, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default) + { + return folder.StoreAsync (indexes, GetStoreFlagsRequest (StoreAction.Add, silent, flags, keywords, modseq), cancellationToken); + } + + /// + /// Remove a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Removes a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The folder. + /// The indexes of the messages. + /// The mod-sequence value. + /// The message flags to remove. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static IList RemoveFlags (this IMailFolder folder, IList indexes, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) + { + return folder.Store (indexes, GetStoreFlagsRequest (StoreAction.Remove, silent, flags, null, modseq), cancellationToken); + } + + /// + /// Asynchronously remove a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously removes a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The folder. + /// The indexes of the messages. + /// The mod-sequence value. + /// The message flags to remove. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task> RemoveFlagsAsync (this IMailFolder folder, IList indexes, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) + { + return folder.StoreAsync (indexes, GetStoreFlagsRequest (StoreAction.Remove, silent, flags, null, modseq), cancellationToken); + } + + /// + /// Remove a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Removes a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The folder. + /// The indexes of the messages. + /// The mod-sequence value. + /// The message flags to remove. + /// A set of user-defined flags to remove. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static IList RemoveFlags (this IMailFolder folder, IList indexes, ulong modseq, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default) + { + return folder.Store (indexes, GetStoreFlagsRequest (StoreAction.Remove, silent, flags, keywords, modseq), cancellationToken); + } + + /// + /// Asynchronously remove a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously removes a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The folder. + /// The indexes of the messages. + /// The mod-sequence value. + /// The message flags to remove. + /// A set of user-defined flags to remove. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task> RemoveFlagsAsync (this IMailFolder folder, IList indexes, ulong modseq, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default) + { + return folder.StoreAsync (indexes, GetStoreFlagsRequest (StoreAction.Remove, silent, flags, keywords, modseq), cancellationToken); + } + + /// + /// Set the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Sets the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The folder. + /// The indexes of the messages. + /// The mod-sequence value. + /// The message flags to set. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static IList SetFlags (this IMailFolder folder, IList indexes, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) + { + return folder.Store (indexes, GetStoreFlagsRequest (StoreAction.Set, silent, flags, null, modseq), cancellationToken); + } + + /// + /// Asynchronously set the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously sets the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The folder. + /// The indexes of the messages. + /// The mod-sequence value. + /// The message flags to set. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task> SetFlagsAsync (this IMailFolder folder, IList indexes, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) + { + return folder.StoreAsync (indexes, GetStoreFlagsRequest (StoreAction.Set, silent, flags, null, modseq), cancellationToken); + } + + /// + /// Set the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Sets the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The folder. + /// The indexes of the messages. + /// The mod-sequence value. + /// The message flags to set. + /// A set of user-defined flags to set. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static IList SetFlags (this IMailFolder folder, IList indexes, ulong modseq, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default) + { + return folder.Store (indexes, GetStoreFlagsRequest (StoreAction.Set, silent, flags, keywords, modseq), cancellationToken); + } + + /// + /// Asynchronously set the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously sets the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The folder. + /// The indexes of the messages. + /// The mod-sequence value. + /// The message flags to set. + /// A set of user-defined flags to set. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task> SetFlagsAsync (this IMailFolder folder, IList indexes, ulong modseq, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default) + { + return folder.StoreAsync (indexes, GetStoreFlagsRequest (StoreAction.Set, silent, flags, keywords, modseq), cancellationToken); + } + + #endregion Store Flags Extensions + + #region Store Labels Extensions + + static StoreLabelsRequest GetStoreLabelsRequest (StoreAction action, bool silent, IList labels, ulong? modseq = null) + { + if (labels == null) + throw new ArgumentNullException (nameof (labels)); + + return new StoreLabelsRequest (action, labels) { + UnchangedSince = modseq, + Silent = silent + }; + } + + /// + /// Add a set of labels to the specified message. + /// + /// + /// Adds a set of labels to the specified message. + /// + /// The folder. + /// The UID of the message. + /// The labels to add. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static void AddLabels (this IMailFolder folder, UniqueId uid, IList labels, bool silent, CancellationToken cancellationToken = default) + { + folder.Store (new[] { uid }, GetStoreLabelsRequest (StoreAction.Add, silent, labels), cancellationToken); + } + + /// + /// Asynchronously add a set of labels to the specified message. + /// + /// + /// Asynchronously adds a set of labels to the specified message. + /// + /// An asynchronous task context. + /// The folder. + /// The UID of the message. + /// The labels to add. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task AddLabelsAsync (this IMailFolder folder, UniqueId uid, IList labels, bool silent, CancellationToken cancellationToken = default) + { + return folder.StoreAsync (new[] { uid }, GetStoreLabelsRequest (StoreAction.Add, silent, labels), cancellationToken); + } + + /// + /// Add a set of labels to the specified messages. + /// + /// + /// Adds a set of labels to the specified messages. + /// + /// The folder. + /// The UIDs of the messages. + /// The labels to add. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static void AddLabels (this IMailFolder folder, IList uids, IList labels, bool silent, CancellationToken cancellationToken = default) + { + folder.Store (uids, GetStoreLabelsRequest (StoreAction.Add, silent, labels), cancellationToken); + } + + /// + /// Asynchronously add a set of labels to the specified messages. + /// + /// + /// Asynchronously adds a set of labels to the specified messages. + /// + /// An asynchronous task context. + /// The folder. + /// The UIDs of the messages. + /// The labels to add. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task AddLabelsAsync (this IMailFolder folder, IList uids, IList labels, bool silent, CancellationToken cancellationToken = default) + { + return folder.StoreAsync (uids, GetStoreLabelsRequest (StoreAction.Add, silent, labels), cancellationToken); + } + + /// + /// Remove a set of labels from the specified message. + /// + /// + /// Removes a set of labels from the specified message. + /// + /// The folder. + /// The UIDs of the message. + /// The labels to remove. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static void RemoveLabels (this IMailFolder folder, UniqueId uid, IList labels, bool silent, CancellationToken cancellationToken = default) + { + folder.Store (new[] { uid }, GetStoreLabelsRequest (StoreAction.Remove, silent, labels), cancellationToken); + } + + /// + /// Asynchronously remove a set of labels from the specified message. + /// + /// + /// Asynchronously removes a set of labels from the specified message. + /// + /// An asynchronous task context. + /// The folder. + /// The UID of the message. + /// The labels to remove. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task RemoveLabelsAsync (this IMailFolder folder, UniqueId uid, IList labels, bool silent, CancellationToken cancellationToken = default) + { + return folder.StoreAsync (new[] { uid }, GetStoreLabelsRequest (StoreAction.Remove, silent, labels), cancellationToken); + } + + /// + /// Remove a set of labels from the specified messages. + /// + /// + /// Removes a set of labels from the specified messages. + /// + /// The folder. + /// The UIDs of the messages. + /// The labels to remove. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static void RemoveLabels (this IMailFolder folder, IList uids, IList labels, bool silent, CancellationToken cancellationToken = default) + { + folder.Store (uids, GetStoreLabelsRequest (StoreAction.Remove, silent, labels), cancellationToken); + } + + /// + /// Asynchronously remove a set of labels from the specified messages. + /// + /// + /// Asynchronously removes a set of labels from the specified messages. + /// + /// An asynchronous task context. + /// The folder. + /// The UIDs of the messages. + /// The labels to remove. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task RemoveLabelsAsync (this IMailFolder folder, IList uids, IList labels, bool silent, CancellationToken cancellationToken = default) + { + return folder.StoreAsync (uids, GetStoreLabelsRequest (StoreAction.Remove, silent, labels), cancellationToken); + } + + /// + /// Set the labels of the specified message. + /// + /// + /// Sets the labels of the specified message. + /// + /// The folder. + /// The UIDs of the message. + /// The labels to set. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static void SetLabels (this IMailFolder folder, UniqueId uid, IList labels, bool silent, CancellationToken cancellationToken = default) + { + folder.Store (new[] { uid }, GetStoreLabelsRequest (StoreAction.Set, silent, labels), cancellationToken); + } + + /// + /// Asynchronously set the labels of the specified message. + /// + /// + /// Asynchronously sets the labels of the specified message. + /// + /// An asynchronous task context. + /// The folder. + /// The UID of the message. + /// The labels to set. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task SetLabelsAsync (this IMailFolder folder, UniqueId uid, IList labels, bool silent, CancellationToken cancellationToken = default) + { + return folder.StoreAsync (new[] { uid }, GetStoreLabelsRequest (StoreAction.Set, silent, labels), cancellationToken); + } + + /// + /// Set the labels of the specified messages. + /// + /// + /// Sets the labels of the specified messages. + /// + /// The folder. + /// The UIDs of the messages. + /// The labels to set. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static void SetLabels (this IMailFolder folder, IList uids, IList labels, bool silent, CancellationToken cancellationToken = default) + { + folder.Store (uids, GetStoreLabelsRequest (StoreAction.Set, silent, labels), cancellationToken); + } + + /// + /// Asynchronously set the labels of the specified messages. + /// + /// + /// Asynchronously sets the labels of the specified messages. + /// + /// An asynchronous task context. + /// The folder. + /// The UIDs of the messages. + /// The labels to set. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task SetLabelsAsync (this IMailFolder folder, IList uids, IList labels, bool silent, CancellationToken cancellationToken = default) + { + return folder.StoreAsync (uids, GetStoreLabelsRequest (StoreAction.Set, silent, labels), cancellationToken); + } + + /// + /// Add a set of labels to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Adds a set of labels to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The folder. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The labels to add. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static IList AddLabels (this IMailFolder folder, IList uids, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default) + { + return folder.Store (uids, GetStoreLabelsRequest (StoreAction.Add, silent, labels, modseq), cancellationToken); + } + + /// + /// Asynchronously add a set of labels to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously adds a set of labels to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The folder. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The labels to add. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task> AddLabelsAsync (this IMailFolder folder, IList uids, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default) + { + return folder.StoreAsync (uids, GetStoreLabelsRequest (StoreAction.Add, silent, labels, modseq), cancellationToken); + } + + /// + /// Remove a set of labels from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Removes a set of labels from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The folder. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The labels to remove. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static IList RemoveLabels (this IMailFolder folder, IList uids, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default) + { + return folder.Store (uids, GetStoreLabelsRequest (StoreAction.Remove, silent, labels, modseq), cancellationToken); + } + + /// + /// Asynchronously remove a set of labels from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously removes a set of labels from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The folder. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The labels to remove. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task> RemoveLabelsAsync (this IMailFolder folder, IList uids, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default) + { + return folder.StoreAsync (uids, GetStoreLabelsRequest (StoreAction.Remove, silent, labels, modseq), cancellationToken); + } + + /// + /// Set the labels of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Sets the labels of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The folder. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The labels to set. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static IList SetLabels (this IMailFolder folder, IList uids, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default) + { + return folder.Store (uids, GetStoreLabelsRequest (StoreAction.Set, silent, labels, modseq), cancellationToken); + } + + /// + /// Asynchronously set the labels of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously sets the labels of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The folder. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The labels to set. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task> SetLabelsAsync (this IMailFolder folder, IList uids, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default) + { + return folder.StoreAsync (uids, GetStoreLabelsRequest (StoreAction.Set, silent, labels, modseq), cancellationToken); + } + + /// + /// Add a set of labels to the specified message. + /// + /// + /// Adds a set of labels to the specified message. + /// + /// The folder. + /// The index of the message. + /// The labels to add. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static void AddLabels (this IMailFolder folder, int index, IList labels, bool silent, CancellationToken cancellationToken = default) + { + folder.Store (new[] { index }, GetStoreLabelsRequest (StoreAction.Add, silent, labels), cancellationToken); + } + + /// + /// Asynchronously add a set of labels to the specified message. + /// + /// + /// Asynchronously adds a set of labels to the specified message. + /// + /// An asynchronous task context. + /// The folder. + /// The index of the messages. + /// The labels to add. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task AddLabelsAsync (this IMailFolder folder, int index, IList labels, bool silent, CancellationToken cancellationToken = default) + { + return folder.StoreAsync (new[] { index }, GetStoreLabelsRequest (StoreAction.Add, silent, labels), cancellationToken); + } + + /// + /// Add a set of labels to the specified messages. + /// + /// + /// Adds a set of labels to the specified messages. + /// + /// The folder. + /// The indexes of the messages. + /// The labels to add. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static void AddLabels (this IMailFolder folder, IList indexes, IList labels, bool silent, CancellationToken cancellationToken = default) + { + folder.Store (indexes, GetStoreLabelsRequest (StoreAction.Add, silent, labels), cancellationToken); + } + + /// + /// Asynchronously add a set of labels to the specified messages. + /// + /// + /// Asynchronously adds a set of labels to the specified messages. + /// + /// An asynchronous task context. + /// The folder. + /// The indexes of the messages. + /// The labels to add. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task AddLabelsAsync (this IMailFolder folder, IList indexes, IList labels, bool silent, CancellationToken cancellationToken = default) + { + return folder.StoreAsync (indexes, GetStoreLabelsRequest (StoreAction.Add, silent, labels), cancellationToken); + } + + /// + /// Remove a set of labels from the specified message. + /// + /// + /// Removes a set of labels from the specified message. + /// + /// The folder. + /// The index of the message. + /// The labels to remove. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static void RemoveLabels (this IMailFolder folder, int index, IList labels, bool silent, CancellationToken cancellationToken = default) + { + folder.Store (new[] { index }, GetStoreLabelsRequest (StoreAction.Remove, silent, labels), cancellationToken); + } + + /// + /// Asynchronously remove a set of labels from the specified message. + /// + /// + /// Asynchronously removes a set of labels from the specified message. + /// + /// An asynchronous task context. + /// The folder. + /// The index of the message. + /// The labels to remove. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task RemoveLabelsAsync (this IMailFolder folder, int index, IList labels, bool silent, CancellationToken cancellationToken = default) + { + return folder.StoreAsync (new[] { index }, GetStoreLabelsRequest (StoreAction.Remove, silent, labels), cancellationToken); + } + + /// + /// Remove a set of labels from the specified messages. + /// + /// + /// Removes a set of labels from the specified messages. + /// + /// The folder. + /// The indexes of the messages. + /// The labels to remove. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static void RemoveLabels (this IMailFolder folder, IList indexes, IList labels, bool silent, CancellationToken cancellationToken = default) + { + folder.Store (indexes, GetStoreLabelsRequest (StoreAction.Remove, silent, labels), cancellationToken); + } + + /// + /// Asynchronously remove a set of labels from the specified messages. + /// + /// + /// Asynchronously removes a set of labels from the specified messages. + /// + /// An asynchronous task context. + /// The folder. + /// The indexes of the messages. + /// The labels to remove. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task RemoveLabelsAsync (this IMailFolder folder, IList indexes, IList labels, bool silent, CancellationToken cancellationToken = default) + { + return folder.StoreAsync (indexes, GetStoreLabelsRequest (StoreAction.Remove, silent, labels), cancellationToken); + } + + /// + /// Set the labels of the specified message. + /// + /// + /// Sets the labels of the specified message. + /// + /// The folder. + /// The index of the message. + /// The labels to set. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static void SetLabels (this IMailFolder folder, int index, IList labels, bool silent, CancellationToken cancellationToken = default) + { + folder.Store (new[] { index }, GetStoreLabelsRequest (StoreAction.Set, silent, labels), cancellationToken); + } + + /// + /// Asynchronously set the labels of the specified message. + /// + /// + /// Asynchronously sets the labels of the specified message. + /// + /// An asynchronous task context. + /// The folder. + /// The index of the message. + /// The labels to set. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task SetLabelsAsync (this IMailFolder folder, int index, IList labels, bool silent, CancellationToken cancellationToken = default) + { + return folder.StoreAsync (new[] { index }, GetStoreLabelsRequest (StoreAction.Set, silent, labels), cancellationToken); + } + + /// + /// Set the labels of the specified messages. + /// + /// + /// Sets the labels of the specified messages. + /// + /// The folder. + /// The indexes of the messages. + /// The labels to set. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static void SetLabels (this IMailFolder folder, IList indexes, IList labels, bool silent, CancellationToken cancellationToken = default) + { + folder.Store (indexes, GetStoreLabelsRequest (StoreAction.Set, silent, labels), cancellationToken); + } + + /// + /// Asynchronously set the labels of the specified messages. + /// + /// + /// Asynchronously sets the labels of the specified messages. + /// + /// An asynchronous task context. + /// The folder. + /// The indexes of the messages. + /// The labels to set. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task SetLabelsAsync (this IMailFolder folder, IList indexes, IList labels, bool silent, CancellationToken cancellationToken = default) + { + return folder.StoreAsync (indexes, GetStoreLabelsRequest (StoreAction.Set, silent, labels), cancellationToken); + } + + /// + /// Add a set of labels to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Adds a set of labels to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The folder. + /// The indexes of the messages. + /// The mod-sequence value. + /// The labels to add. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static IList AddLabels (this IMailFolder folder, IList indexes, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default) + { + return folder.Store (indexes, GetStoreLabelsRequest (StoreAction.Add, silent, labels, modseq), cancellationToken); + } + + /// + /// Asynchronously add a set of labels to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously adds a set of labels to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The folder. + /// The indexes of the messages. + /// The mod-sequence value. + /// The labels to add. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task> AddLabelsAsync (this IMailFolder folder, IList indexes, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default) + { + return folder.StoreAsync (indexes, GetStoreLabelsRequest (StoreAction.Add, silent, labels, modseq), cancellationToken); + } + + /// + /// Remove a set of labels from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Removes a set of labels from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The folder. + /// The indexes of the messages. + /// The mod-sequence value. + /// The labels to remove. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static IList RemoveLabels (this IMailFolder folder, IList indexes, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default) + { + return folder.Store (indexes, GetStoreLabelsRequest (StoreAction.Remove, silent, labels, modseq), cancellationToken); + } + + /// + /// Asynchronously remove a set of labels from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously removes a set of labels from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The folder. + /// The indexes of the messages. + /// The mod-sequence value. + /// The labels to remove. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task> RemoveLabelsAsync (this IMailFolder folder, IList indexes, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default) + { + return folder.StoreAsync (indexes, GetStoreLabelsRequest (StoreAction.Remove, silent, labels, modseq), cancellationToken); + } + + /// + /// Set the labels of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Sets the labels of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The folder. + /// The indexes of the messages. + /// The mod-sequence value. + /// The labels to set. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static IList SetLabels (this IMailFolder folder, IList indexes, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default) + { + return folder.Store (indexes, GetStoreLabelsRequest (StoreAction.Set, silent, labels, modseq), cancellationToken); + } + + /// + /// Asynchronously set the labels of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously sets the labels of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The folder. + /// The indexes of the messages. + /// The mod-sequence value. + /// The labels to set. + /// If set to , no events will be emitted. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public static Task> SetLabelsAsync (this IMailFolder folder, IList indexes, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default) + { + return folder.StoreAsync (indexes, GetStoreLabelsRequest (StoreAction.Set, silent, labels, modseq), cancellationToken); + } + + #endregion Store Labels Extensions + } +} diff --git a/MailKit/IMailService.cs b/MailKit/IMailService.cs index 4fa9c7bd05..f3455fd36f 100644 --- a/MailKit/IMailService.cs +++ b/MailKit/IMailService.cs @@ -1,9 +1,9 @@ -// +// // IMailService.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -25,18 +25,19 @@ // using System; +using System.IO; using System.Net; using System.Text; using System.Threading; +using System.Net.Sockets; +using System.Net.Security; using System.Threading.Tasks; using System.Collections.Generic; - -#if !NETFX_CORE -using System.Net.Security; +using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; -#else -using Encoding = Portable.Text.Encoding; -#endif +using SslProtocols = System.Security.Authentication.SslProtocols; + +using MailKit.Net.Proxy; using MailKit.Security; @@ -45,13 +46,14 @@ namespace MailKit { /// An interface for message services such as SMTP, POP3, or IMAP. /// /// - /// Implemented by - /// and . + /// Implemented by , + /// and + /// . /// public interface IMailService : IDisposable { /// - /// Gets an object that can be used to synchronize access to the folder. + /// Get an object that can be used to synchronize access to the folder. /// /// /// Gets an object that can be used to synchronize access to the folder. @@ -59,47 +61,119 @@ public interface IMailService : IDisposable /// The sync root. object SyncRoot { get; } -#if !NETFX_CORE + /// + /// Get or set the set of enabled SSL and/or TLS protocol versions that the client is allowed to use. + /// + /// + /// Gets or sets the enabled SSL and/or TLS protocol versions that the client is allowed to use. + /// By default, MailKit initializes this value to which allows the + /// operating system to choose the best protocol to use and to block protocols that are not secure. + /// This property should be set before calling any of the + /// Connect or + /// ConnectAsync methods. + /// + /// The SSL and TLS protocol versions that are supported. + SslProtocols SslProtocols { get; set; } + +#if NET5_0_OR_GREATER + /// + /// Get or set the cipher suites allowed to be used when negotiating an SSL or TLS connection. + /// + /// + /// Specifies the cipher suites allowed to be used when negotiating an SSL or TLS connection. + /// When set to , the operating system default is used. Use extreme caution when + /// changing this setting. + /// This property should be set before calling any of the + /// Connect or + /// ConnectAsync methods. + /// + /// The cipher algorithms allowed for use when negotiating SSL or TLS encryption. + CipherSuitesPolicy? SslCipherSuitesPolicy { get; set; } + + /// + /// Get the negotiated SSL or TLS cipher suite. + /// + /// + /// Gets the negotiated SSL or TLS cipher suite once an SSL or TLS connection has been made. + /// + /// The negotiated SSL or TLS cipher suite. + TlsCipherSuite? SslCipherSuite { get; } +#endif + /// /// Get or set the client SSL certificates. /// /// /// Some servers may require the client SSL certificates in order /// to allow the user to connect. - /// This property should be set before calling . + /// This property should be set before calling any of the + /// Connect or + /// ConnectAsync methods. /// /// The client SSL certificates. - X509CertificateCollection ClientCertificates { get; set; } + X509CertificateCollection? ClientCertificates { get; set; } + + /// + /// Get or set whether connecting via SSL/TLS should check certificate revocation. + /// + /// + /// Gets or sets whether connecting via SSL/TLS should check certificate revocation. + /// Normally, the value of this property should be set to (the default) for security + /// reasons, but there are times when it may be necessary to set it to . + /// For example, most Certificate Authorities are probably pretty good at keeping their CRL and/or + /// OCSP servers up 24/7, but occasionally they do go down or are otherwise unreachable due to other + /// network problems between the client and the Certificate Authority. When this happens, it becomes + /// impossible to check the revocation status of one or more of the certificates in the chain + /// resulting in an being thrown in the + /// Connect method. If this becomes a problem, + /// it may become desirable to set to . + /// + /// if certificate revocation should be checked; otherwise, . + bool CheckCertificateRevocation { get; set; } /// - /// Get or sets a callback function to validate the server certificate. + /// Get or set a callback function to validate the server certificate. /// /// /// Gets or sets a callback function to validate the server certificate. - /// This property should be set before calling . + /// This property should be set before calling any of the + /// Connect or + /// ConnectAsync methods. /// /// - /// + /// /// /// The server certificate validation callback function. - RemoteCertificateValidationCallback ServerCertificateValidationCallback { get; set; } + RemoteCertificateValidationCallback? ServerCertificateValidationCallback { get; set; } /// - /// Get or set the local IP end point to use when connecting to the remote host. + /// Get or set the local IP end point to use when connecting to a remote host. /// /// - /// Gets or sets the local IP end point to use when connecting to the remote host. + /// Gets or sets the local IP end point to use when connecting to a remote host. /// - /// The local IP end point or null to use the default end point. - IPEndPoint LocalEndPoint { get; set; } -#endif + /// The local IP end point or to use the default end point. + IPEndPoint? LocalEndPoint { get; set; } + + /// + /// Get or set the proxy client to use when connecting to a remote host. + /// + /// + /// Gets or sets the proxy client to use when connecting to a remote host via any of the + /// Connect methods. + /// + /// + /// + /// + /// The proxy client. + IProxyClient? ProxyClient { get; set; } /// /// Get the authentication mechanisms supported by the message service. /// /// - /// The authentication mechanisms are queried durring the - /// method. + /// The authentication mechanisms are queried during the + /// Connect method. /// /// The supported authentication mechanisms. HashSet AuthenticationMechanisms { get; } @@ -113,23 +187,23 @@ public interface IMailService : IDisposable /// Authenticate methods /// or any of the Async alternatives. /// - /// true if the client is authenticated; otherwise, false. + /// if the client is authenticated; otherwise, . bool IsAuthenticated { get; } /// /// Get whether or not the service is currently connected. /// /// - /// The state is set to true immediately after + /// The state is set to immediately after /// one of the Connect - /// methods succeeds and is not set back to false until either the client + /// methods succeeds and is not set back to until either the client /// is disconnected via or until a /// is thrown while attempting to read or write to /// the underlying network socket. /// When an is caught, the connection state of the /// should be checked before continuing. /// - /// true if the service connected; otherwise, false. + /// if the service connected; otherwise, . bool IsConnected { get; } /// @@ -138,9 +212,108 @@ public interface IMailService : IDisposable /// /// Gets whether or not the connection is secure (typically via SSL or TLS). /// - /// true if the connection is secure; otherwise, false. + /// if the connection is secure; otherwise, . bool IsSecure { get; } + /// + /// Get whether or not the connection is encrypted (typically via SSL or TLS). + /// + /// + /// Gets whether or not the connection is encrypted (typically via SSL or TLS). + /// + /// if the connection is encrypted; otherwise, . + bool IsEncrypted { get; } + + /// + /// Get whether or not the connection is signed (typically via SSL or TLS). + /// + /// + /// Gets whether or not the connection is signed (typically via SSL or TLS). + /// + /// if the connection is signed; otherwise, . + bool IsSigned { get; } + + /// + /// Get the negotiated SSL or TLS protocol version. + /// + /// + /// Gets the negotiated SSL or TLS protocol version once an SSL or TLS connection has been made. + /// + /// The negotiated SSL or TLS protocol version. + SslProtocols SslProtocol { get; } + + /// + /// Get the negotiated SSL or TLS cipher algorithm. + /// + /// + /// Gets the negotiated SSL or TLS cipher algorithm once an SSL or TLS connection has been made. + /// + /// The negotiated SSL or TLS cipher algorithm. +#if NET10_0_OR_GREATER + [Obsolete ("Use SslCipherSuite instead.")] +#endif + CipherAlgorithmType? SslCipherAlgorithm { get; } + + /// + /// Get the negotiated SSL or TLS cipher algorithm strength. + /// + /// + /// Gets the negotiated SSL or TLS cipher algorithm strength once an SSL or TLS connection has been made. + /// + /// The negotiated SSL or TLS cipher algorithm strength. +#if NET10_0_OR_GREATER + [Obsolete ("Use SslCipherSuite instead.")] +#endif + int? SslCipherStrength { get; } + + /// + /// Get the negotiated SSL or TLS hash algorithm. + /// + /// + /// Gets the negotiated SSL or TLS hash algorithm once an SSL or TLS connection has been made. + /// + /// The negotiated SSL or TLS hash algorithm. +#if NET10_0_OR_GREATER + [Obsolete ("Use SslCipherSuite instead.")] +#endif + HashAlgorithmType? SslHashAlgorithm { get; } + + /// + /// Get the negotiated SSL or TLS hash algorithm strength. + /// + /// + /// Gets the negotiated SSL or TLS hash algorithm strength once an SSL or TLS connection has been made. + /// + /// The negotiated SSL or TLS hash algorithm strength. +#if NET10_0_OR_GREATER + [Obsolete ("Use SslCipherSuite instead.")] +#endif + int? SslHashStrength { get; } + + /// + /// Get the negotiated SSL or TLS key exchange algorithm. + /// + /// + /// Gets the negotiated SSL or TLS key exchange algorithm once an SSL or TLS connection has been made. + /// + /// The negotiated SSL or TLS key exchange algorithm. +#if NET10_0_OR_GREATER + [Obsolete ("Use SslCipherSuite instead.")] +#endif + ExchangeAlgorithmType? SslKeyExchangeAlgorithm { get; } + + /// + /// Get the negotiated SSL or TLS key exchange algorithm strength. + /// + /// + /// Gets the negotiated SSL or TLS key exchange algorithm strength once an SSL or TLS connection has been made. + /// + /// The negotiated SSL or TLS key exchange algorithm strength. +#if NET10_0_OR_GREATER + [Obsolete ("Use SslCipherSuite instead.")] +#endif + int? SslKeyExchangeStrength { get; } + /// /// Get or set the timeout for network streaming operations, in milliseconds. /// @@ -161,14 +334,17 @@ public interface IMailService : IDisposable /// /// The host name to connect to. /// The port to connect to. If the specified port is 0, then the default port will be used. - /// true if the client should make an SSL-wrapped connection to the server; otherwise, false. + /// if the client should make an SSL-wrapped connection to the server; otherwise, . /// The cancellation token. /// - /// is null. + /// is . /// /// /// is not between 0 and 65535. /// + /// + /// The is a zero-length string. + /// /// /// The is already connected. /// @@ -184,7 +360,7 @@ public interface IMailService : IDisposable /// /// The server responded with an unexpected token. /// - void Connect (string host, int port, bool useSsl, CancellationToken cancellationToken = default (CancellationToken)); + void Connect (string host, int port, bool useSsl, CancellationToken cancellationToken = default); /// /// Asynchronously establish a connection to the specified mail server. @@ -197,14 +373,17 @@ public interface IMailService : IDisposable /// An asynchronous task context. /// The host name to connect to. /// The port to connect to. If the specified port is 0, then the default port will be used. - /// true if the client should make an SSL-wrapped connection to the server; otherwise, false. + /// if the client should make an SSL-wrapped connection to the server; otherwise, . /// The cancellation token. /// - /// The is null. + /// The is . /// /// /// is not between 0 and 65535. /// + /// + /// The is a zero-length string. + /// /// /// The has been disposed. /// @@ -220,7 +399,7 @@ public interface IMailService : IDisposable /// /// A protocol error occurred. /// - Task ConnectAsync (string host, int port, bool useSsl, CancellationToken cancellationToken = default (CancellationToken)); + Task ConnectAsync (string host, int port, bool useSsl, CancellationToken cancellationToken = default); /// /// Establish a connection to the specified mail server. @@ -235,11 +414,14 @@ public interface IMailService : IDisposable /// The secure socket options to when connecting. /// The cancellation token. /// - /// is null. + /// is . /// /// /// is not between 0 and 65535. /// + /// + /// The is a zero-length string. + /// /// /// The is already connected. /// @@ -255,7 +437,7 @@ public interface IMailService : IDisposable /// /// The server responded with an unexpected token. /// - void Connect (string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default (CancellationToken)); + void Connect (string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default); /// /// Asynchronously establish a connection to the specified mail server. @@ -271,11 +453,14 @@ public interface IMailService : IDisposable /// The secure socket options to when connecting. /// The cancellation token. /// - /// The is null. + /// The is . /// /// /// is not between 0 and 65535. /// + /// + /// The is a zero-length string. + /// /// /// The has been disposed. /// @@ -291,7 +476,177 @@ public interface IMailService : IDisposable /// /// A protocol error occurred. /// - Task ConnectAsync (string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default (CancellationToken)); + Task ConnectAsync (string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default); + + /// + /// Establish a connection to the specified mail server using the provided socket. + /// + /// + /// Establish a connection to the specified mail server using the provided socket. + /// If a successful connection is made, the + /// property will be populated. + /// + /// The socket to use for the connection. + /// The host name to connect to. + /// The port to connect to. If the specified port is 0, then the default port will be used. + /// The secure socket options to when connecting. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// is not between 0 and 65535. + /// + /// + /// is not connected. + /// -or- + /// The is a zero-length string. + /// + /// + /// The is already connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command was rejected by the mail server. + /// + /// + /// The server responded with an unexpected token. + /// + void Connect (Socket socket, string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default); + + /// + /// Asynchronously establish a connection to the specified mail server using the provided socket. + /// + /// + /// Asynchronously establishes a connection to the specified mail server using the provided socket. + /// If a successful connection is made, the + /// property will be populated. + /// + /// An asynchronous task context. + /// The socket to use for the connection. + /// The host name to connect to. + /// The port to connect to. If the specified port is 0, then the default port will be used. + /// The secure socket options to when connecting. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// is not between 0 and 65535. + /// + /// + /// is not connected. + /// -or- + /// The is a zero-length string. + /// + /// + /// The is already connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command was rejected by the mail server. + /// + /// + /// The server responded with an unexpected token. + /// + Task ConnectAsync (Socket socket, string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default); + + /// + /// Establish a connection to the specified mail server using the provided stream. + /// + /// + /// Establish a connection to the specified mail server using the provided stream. + /// If a successful connection is made, the + /// property will be populated. + /// + /// The stream to use for the connection. + /// The host name to connect to. + /// The port to connect to. If the specified port is 0, then the default port will be used. + /// The secure socket options to when connecting. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The is already connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command was rejected by the mail server. + /// + /// + /// The server responded with an unexpected token. + /// + void Connect (Stream stream, string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default); + + /// + /// Asynchronously establish a connection to the specified mail server using the provided stream. + /// + /// + /// Asynchronously establishes a connection to the specified mail server using the provided stream. + /// If a successful connection is made, the + /// property will be populated. + /// + /// An asynchronous task context. + /// The stream to use for the connection. + /// The host name to connect to. + /// The port to connect to. If the specified port is 0, then the default port will be used. + /// The secure socket options to when connecting. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The is already connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command was rejected by the mail server. + /// + /// + /// The server responded with an unexpected token. + /// + Task ConnectAsync (Stream stream, string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default); /// /// Authenticate using the supplied credentials. @@ -299,9 +654,9 @@ public interface IMailService : IDisposable /// /// Authenticates using the supplied credentials. /// If the server supports one or more SASL authentication mechanisms, then - /// the SASL mechanisms that both the client and server support are tried - /// in order of greatest security to weakest security. Once a SASL - /// authentication mechanism is found that both client and server support, + /// the SASL mechanisms that both the client and server support (not including any + /// OAUTH mechanisms) are tried in order of greatest security to weakest security. + /// Once a SASL authentication mechanism is found that both client and server support, /// the credentials are used to authenticate. /// If the server does not support SASL or if no common SASL mechanisms /// can be found, then the default login command is used as a fallback. @@ -309,7 +664,7 @@ public interface IMailService : IDisposable /// The user's credentials. /// The cancellation token. /// - /// is null. + /// is . /// /// /// The has been disposed. @@ -335,7 +690,7 @@ public interface IMailService : IDisposable /// /// A protocol error occurred. /// - void Authenticate (ICredentials credentials, CancellationToken cancellationToken = default (CancellationToken)); + void Authenticate (ICredentials credentials, CancellationToken cancellationToken = default); /// /// Asynchronously authenticate using the supplied credentials. @@ -343,9 +698,9 @@ public interface IMailService : IDisposable /// /// Asynchronously authenticates using the supplied credentials. /// If the server supports one or more SASL authentication mechanisms, then - /// the SASL mechanisms that both the client and server support are tried - /// in order of greatest security to weakest security. Once a SASL - /// authentication mechanism is found that both client and server support, + /// the SASL mechanisms that both the client and server support (not including any + /// OAUTH mechanisms) are tried in order of greatest security to weakest security. + /// Once a SASL authentication mechanism is found that both client and server support, /// the credentials are used to authenticate. /// If the server does not support SASL or if no common SASL mechanisms /// can be found, then the default login command is used as a fallback. @@ -354,7 +709,7 @@ public interface IMailService : IDisposable /// The user's credentials. /// The cancellation token. /// - /// is null. + /// is . /// /// /// The has been disposed. @@ -380,7 +735,7 @@ public interface IMailService : IDisposable /// /// A protocol error occurred. /// - Task AuthenticateAsync (ICredentials credentials, CancellationToken cancellationToken = default (CancellationToken)); + Task AuthenticateAsync (ICredentials credentials, CancellationToken cancellationToken = default); /// /// Authenticate using the supplied credentials. @@ -388,9 +743,9 @@ public interface IMailService : IDisposable /// /// Authenticates using the supplied credentials. /// If the server supports one or more SASL authentication mechanisms, then - /// the SASL mechanisms that both the client and server support are tried - /// in order of greatest security to weakest security. Once a SASL - /// authentication mechanism is found that both client and server support, + /// the SASL mechanisms that both the client and server support (not including any + /// OAUTH mechanisms) are tried in order of greatest security to weakest security. + /// Once a SASL authentication mechanism is found that both client and server support, /// the credentials are used to authenticate. /// If the server does not support SASL or if no common SASL mechanisms /// can be found, then the default login command is used as a fallback. @@ -399,9 +754,9 @@ public interface IMailService : IDisposable /// The user's credentials. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// The has been disposed. @@ -427,7 +782,7 @@ public interface IMailService : IDisposable /// /// A protocol error occurred. /// - void Authenticate (Encoding encoding, ICredentials credentials, CancellationToken cancellationToken = default (CancellationToken)); + void Authenticate (Encoding encoding, ICredentials credentials, CancellationToken cancellationToken = default); /// /// Asynchronously authenticate using the supplied credentials. @@ -435,9 +790,9 @@ public interface IMailService : IDisposable /// /// Asynchronously authenticates using the supplied credentials. /// If the server supports one or more SASL authentication mechanisms, then - /// the SASL mechanisms that both the client and server support are tried - /// in order of greatest security to weakest security. Once a SASL - /// authentication mechanism is found that both client and server support, + /// the SASL mechanisms that both the client and server support (not including any + /// OAUTH mechanisms) are tried in order of greatest security to weakest security. + /// Once a SASL authentication mechanism is found that both client and server support, /// the credentials are used to authenticate. /// If the server does not support SASL or if no common SASL mechanisms /// can be found, then the default login command is used as a fallback. @@ -447,9 +802,9 @@ public interface IMailService : IDisposable /// The user's credentials. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// The has been disposed. @@ -475,16 +830,17 @@ public interface IMailService : IDisposable /// /// A protocol error occurred. /// - Task AuthenticateAsync (Encoding encoding, ICredentials credentials, CancellationToken cancellationToken = default (CancellationToken)); + Task AuthenticateAsync (Encoding encoding, ICredentials credentials, CancellationToken cancellationToken = default); /// - /// Authenticates using the specified user name and password. + /// Authenticate using the specified user name and password. /// /// - /// If the server supports one or more SASL authentication mechanisms, - /// then the SASL mechanisms that both the client and server support are tried - /// in order of greatest security to weakest security. Once a SASL - /// authentication mechanism is found that both client and server support, + /// Authenticates using the supplied credentials. + /// If the server supports one or more SASL authentication mechanisms, then + /// the SASL mechanisms that both the client and server support (not including any + /// OAUTH mechanisms) are tried in order of greatest security to weakest security. + /// Once a SASL authentication mechanism is found that both client and server support, /// the credentials are used to authenticate. /// If the server does not support SASL or if no common SASL mechanisms /// can be found, then the default login command is used as a fallback. @@ -497,11 +853,11 @@ public interface IMailService : IDisposable /// The password. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// The has been disposed. @@ -524,16 +880,17 @@ public interface IMailService : IDisposable /// /// A protocol error occurred. /// - void Authenticate (Encoding encoding, string userName, string password, CancellationToken cancellationToken = default (CancellationToken)); + void Authenticate (Encoding encoding, string userName, string password, CancellationToken cancellationToken = default); /// - /// Asynchronously authenticates using the specified user name and password. + /// Asynchronously authenticate using the specified user name and password. /// /// - /// If the server supports one or more SASL authentication mechanisms, - /// then the SASL mechanisms that both the client and server support are tried - /// in order of greatest security to weakest security. Once a SASL - /// authentication mechanism is found that both client and server support, + /// Asynchronously authenticates using the supplied credentials. + /// If the server supports one or more SASL authentication mechanisms, then + /// the SASL mechanisms that both the client and server support (not including any + /// OAUTH mechanisms) are tried in order of greatest security to weakest security. + /// Once a SASL authentication mechanism is found that both client and server support, /// the credentials are used to authenticate. /// If the server does not support SASL or if no common SASL mechanisms /// can be found, then the default login command is used as a fallback. @@ -547,11 +904,11 @@ public interface IMailService : IDisposable /// The password. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// The has been disposed. @@ -574,16 +931,17 @@ public interface IMailService : IDisposable /// /// A protocol error occurred. /// - Task AuthenticateAsync (Encoding encoding, string userName, string password, CancellationToken cancellationToken = default (CancellationToken)); + Task AuthenticateAsync (Encoding encoding, string userName, string password, CancellationToken cancellationToken = default); /// - /// Authenticates using the specified user name and password. + /// Authenticate using the specified user name and password. /// /// - /// If the server supports one or more SASL authentication mechanisms, - /// then the SASL mechanisms that both the client and server support are tried - /// in order of greatest security to weakest security. Once a SASL - /// authentication mechanism is found that both client and server support, + /// Authenticates using the supplied credentials. + /// If the server supports one or more SASL authentication mechanisms, then + /// the SASL mechanisms that both the client and server support (not including any + /// OAUTH mechanisms) are tried in order of greatest security to weakest security. + /// Once a SASL authentication mechanism is found that both client and server support, /// the credentials are used to authenticate. /// If the server does not support SASL or if no common SASL mechanisms /// can be found, then the default login command is used as a fallback. @@ -598,9 +956,9 @@ public interface IMailService : IDisposable /// The password. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// The has been disposed. @@ -623,16 +981,17 @@ public interface IMailService : IDisposable /// /// A protocol error occurred. /// - void Authenticate (string userName, string password, CancellationToken cancellationToken = default (CancellationToken)); + void Authenticate (string userName, string password, CancellationToken cancellationToken = default); /// - /// Asynchronously authenticates using the specified user name and password. + /// Asynchronously authenticate using the specified user name and password. /// /// - /// If the server supports one or more SASL authentication mechanisms, - /// then the SASL mechanisms that both the client and server support are tried - /// in order of greatest security to weakest security. Once a SASL - /// authentication mechanism is found that both client and server support, + /// Asynchronously authenticates using the supplied credentials. + /// If the server supports one or more SASL authentication mechanisms, then + /// the SASL mechanisms that both the client and server support (not including any + /// OAUTH mechanisms) are tried in order of greatest security to weakest security. + /// Once a SASL authentication mechanism is found that both client and server support, /// the credentials are used to authenticate. /// If the server does not support SASL or if no common SASL mechanisms /// can be found, then the default login command is used as a fallback. @@ -645,9 +1004,84 @@ public interface IMailService : IDisposable /// The password. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected or is already authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Authentication using the supplied credentials has failed. + /// + /// + /// A SASL authentication error occurred. + /// + /// + /// An I/O error occurred. + /// + /// + /// A protocol error occurred. + /// + Task AuthenticateAsync (string userName, string password, CancellationToken cancellationToken = default); + + /// + /// Authenticate using the specified SASL mechanism. + /// + /// + /// Authenticates using the specified SASL mechanism. + /// For a list of available SASL authentication mechanisms supported by the server, + /// check the property after the service has been + /// connected. + /// + /// The SASL mechanism. + /// The cancellation token. + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected or is already authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Authentication using the supplied credentials has failed. + /// + /// + /// A SASL authentication error occurred. + /// + /// + /// An I/O error occurred. + /// + /// + /// A protocol error occurred. + /// + void Authenticate (SaslMechanism mechanism, CancellationToken cancellationToken = default); + + /// + /// Asynchronously authenticate using the specified SASL mechanism. + /// + /// + /// Authenticates using the specified SASL mechanism. + /// For a list of available SASL authentication mechanisms supported by the server, + /// check the property after the service has been + /// connected. + /// + /// An asynchronous task context. + /// The SASL mechanism. + /// The cancellation token. + /// + /// is . /// /// /// The has been disposed. @@ -670,16 +1104,16 @@ public interface IMailService : IDisposable /// /// A protocol error occurred. /// - Task AuthenticateAsync (string userName, string password, CancellationToken cancellationToken = default (CancellationToken)); + Task AuthenticateAsync (SaslMechanism mechanism, CancellationToken cancellationToken = default); /// /// Disconnect the service. /// /// /// Disconnects from the service. - /// If is true, a "QUIT" command will be issued in order to disconnect cleanly. + /// If is , a "QUIT" command will be issued in order to disconnect cleanly. /// - /// If set to true, a "QUIT" command will be issued in order to disconnect cleanly. + /// If set to , a "QUIT" command will be issued in order to disconnect cleanly. /// The cancellation token. /// /// The has been disposed. @@ -699,17 +1133,17 @@ public interface IMailService : IDisposable /// /// The server responded with an unexpected token. /// - void Disconnect (bool quit, CancellationToken cancellationToken = default (CancellationToken)); + void Disconnect (bool quit, CancellationToken cancellationToken = default); /// /// Asynchronously disconnect the service. /// /// /// Asynchronously disconnects from the service. - /// If is true, a "QUIT" command will be issued in order to disconnect cleanly. + /// If is , a "QUIT" command will be issued in order to disconnect cleanly. /// /// An asynchronous task context. - /// If set to true, a logout/quit command will be issued in order to disconnect cleanly. + /// If set to , a logout/quit command will be issued in order to disconnect cleanly. /// The cancellation token. /// /// The has been disposed. @@ -729,7 +1163,7 @@ public interface IMailService : IDisposable /// /// The server responded with an unexpected token. /// - Task DisconnectAsync (bool quit, CancellationToken cancellationToken = default (CancellationToken)); + Task DisconnectAsync (bool quit, CancellationToken cancellationToken = default); /// /// Ping the message service to keep the connection alive. @@ -756,7 +1190,7 @@ public interface IMailService : IDisposable /// /// The server responded with an unexpected token. /// - void NoOp (CancellationToken cancellationToken = default (CancellationToken)); + void NoOp (CancellationToken cancellationToken = default); /// /// Asynchronously ping the mail server to keep the connection alive. @@ -787,7 +1221,7 @@ public interface IMailService : IDisposable /// /// The server responded with an unexpected token. /// - Task NoOpAsync (CancellationToken cancellationToken = default (CancellationToken)); + Task NoOpAsync (CancellationToken cancellationToken = default); /// /// Occurs when the client has been successfully connected. @@ -796,7 +1230,7 @@ public interface IMailService : IDisposable /// The event is raised when the client /// successfully connects to the mail server. /// - event EventHandler Connected; + event EventHandler? Connected; /// /// Occurs when the client has been disconnected. @@ -805,7 +1239,7 @@ public interface IMailService : IDisposable /// The event is raised whenever the client /// has been disconnected. /// - event EventHandler Disconnected; + event EventHandler? Disconnected; /// /// Occurs when the client has been successfully authenticated. @@ -814,6 +1248,6 @@ public interface IMailService : IDisposable /// The event is raised whenever the client /// has been authenticated. /// - event EventHandler Authenticated; + event EventHandler? Authenticated; } } diff --git a/MailKit/IMailSpool.cs b/MailKit/IMailSpool.cs index cdba1e97b3..04ee57b555 100644 --- a/MailKit/IMailSpool.cs +++ b/MailKit/IMailSpool.cs @@ -1,9 +1,9 @@ -// +// // IMailSpool.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -24,7 +24,6 @@ // THE SOFTWARE. // -using System; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -34,10 +33,11 @@ namespace MailKit { /// - /// An interface for retreiving messages from a spool. + /// An interface for retrieving messages from a spool. /// /// - /// An interface for retreiving messages from a spool. + /// An interface for retrieving messages from a spool. + /// Implemented by . /// public interface IMailSpool : IMailService, IEnumerable { @@ -63,30 +63,28 @@ public interface IMailSpool : IMailService, IEnumerable /// along with and /// will fail. /// - /// true if supports uids; otherwise, false. + /// if supports uids; otherwise, . bool SupportsUids { get; } /// - /// Get the number of messages available in the message spool. + /// Get the message count. /// /// - /// Gets the number of messages available in the message spool. + /// Gets the message count. /// - /// The number of available messages. + /// The message count. /// The cancellation token. - [Obsolete ("Use the Count property instead.")] - int GetMessageCount (CancellationToken cancellationToken = default (CancellationToken)); + int GetMessageCount (CancellationToken cancellationToken = default); /// - /// Asynchronously get the number of messages available in the message spool. + /// Asynchronously get the message count. /// /// - /// Asynchronously gets the number of messages available in the message spool. + /// Asynchronously gets the message count. /// - /// The number of available messages. + /// The message count. /// The cancellation token. - [Obsolete ("Use the Count property instead.")] - Task GetMessageCountAsync (CancellationToken cancellationToken = default (CancellationToken)); + Task GetMessageCountAsync (CancellationToken cancellationToken = default); /// /// Get the UID of the message at the specified index. @@ -98,7 +96,7 @@ public interface IMailSpool : IMailService, IEnumerable /// The message UID. /// The message index. /// The cancellation token. - string GetMessageUid (int index, CancellationToken cancellationToken = default (CancellationToken)); + string GetMessageUid (int index, CancellationToken cancellationToken = default); /// /// Asynchronously get the UID of the message at the specified index. @@ -110,7 +108,7 @@ public interface IMailSpool : IMailService, IEnumerable /// The message UID. /// The message index. /// The cancellation token. - Task GetMessageUidAsync (int index, CancellationToken cancellationToken = default (CancellationToken)); + Task GetMessageUidAsync (int index, CancellationToken cancellationToken = default); /// /// Get the full list of available message UIDs. @@ -121,7 +119,7 @@ public interface IMailSpool : IMailService, IEnumerable /// /// The message UIDs. /// The cancellation token. - IList GetMessageUids (CancellationToken cancellationToken = default (CancellationToken)); + IList GetMessageUids (CancellationToken cancellationToken = default); /// /// Asynchronously get the full list of available message UIDs. @@ -132,31 +130,7 @@ public interface IMailSpool : IMailService, IEnumerable /// /// The message UIDs. /// The cancellation token. - Task> GetMessageUidsAsync (CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Get the size of the specified message, in bytes. - /// - /// - /// Gets the size of the specified message, in bytes. - /// - /// The message size, in bytes. - /// The UID of the message. - /// The cancellation token. - [Obsolete ("Use GetMessageSize (int index, CancellationToken cancellationToken) instead.")] - int GetMessageSize (string uid, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously get the size of the specified message, in bytes. - /// - /// - /// Asynchronously gets the size of the specified message, in bytes. - /// - /// The message size, in bytes. - /// The UID of the message. - /// The cancellation token. - [Obsolete ("Use GetMessageSizeAsync (int index, CancellationToken cancellationToken) instead.")] - Task GetMessageSizeAsync (string uid, CancellationToken cancellationToken = default (CancellationToken)); + Task> GetMessageUidsAsync (CancellationToken cancellationToken = default); /// /// Get the size of the specified message, in bytes. @@ -167,7 +141,7 @@ public interface IMailSpool : IMailService, IEnumerable /// The message size, in bytes. /// The index of the message. /// The cancellation token. - int GetMessageSize (int index, CancellationToken cancellationToken = default (CancellationToken)); + int GetMessageSize (int index, CancellationToken cancellationToken = default); /// /// Asynchronously get the size of the specified message, in bytes. @@ -178,7 +152,7 @@ public interface IMailSpool : IMailService, IEnumerable /// The message size, in bytes. /// The index of the message. /// The cancellation token. - Task GetMessageSizeAsync (int index, CancellationToken cancellationToken = default (CancellationToken)); + Task GetMessageSizeAsync (int index, CancellationToken cancellationToken = default); /// /// Get the sizes for all available messages, in bytes. @@ -188,7 +162,7 @@ public interface IMailSpool : IMailService, IEnumerable /// /// The message sizes, in bytes. /// The cancellation token. - IList GetMessageSizes (CancellationToken cancellationToken = default (CancellationToken)); + IList GetMessageSizes (CancellationToken cancellationToken = default); /// /// Asynchronously get the sizes for all available messages, in bytes. @@ -198,31 +172,7 @@ public interface IMailSpool : IMailService, IEnumerable /// /// The message sizes, in bytes. /// The cancellation token. - Task> GetMessageSizesAsync (CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Get the headers for the specified message. - /// - /// - /// Gets the headers for the specified message. - /// - /// The message headers. - /// The UID of the message. - /// The cancellation token. - [Obsolete ("Use GetMessageHeaders (int index, CancellationToken cancellationToken) instead.")] - HeaderList GetMessageHeaders (string uid, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously get the headers for the specified message. - /// - /// - /// Asynchronously gets the headers for the specified message. - /// - /// The message headers. - /// The UID of the message. - /// The cancellation token. - [Obsolete ("Use GetMessageHeadersAsync (int index, CancellationToken cancellationToken) instead.")] - Task GetMessageHeadersAsync (string uid, CancellationToken cancellationToken = default (CancellationToken)); + Task> GetMessageSizesAsync (CancellationToken cancellationToken = default); /// /// Get the headers for the specified message. @@ -233,7 +183,7 @@ public interface IMailSpool : IMailService, IEnumerable /// The message headers. /// The index of the message. /// The cancellation token. - HeaderList GetMessageHeaders (int index, CancellationToken cancellationToken = default (CancellationToken)); + HeaderList GetMessageHeaders (int index, CancellationToken cancellationToken = default); /// /// Asynchronously get the headers for the specified message. @@ -244,31 +194,7 @@ public interface IMailSpool : IMailService, IEnumerable /// The message headers. /// The index of the message. /// The cancellation token. - Task GetMessageHeadersAsync (int index, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Get the headers for the specified messages. - /// - /// - /// Gets the headers for the specified messages. - /// - /// The headers for the specified messages. - /// The UIDs of the messages. - /// The cancellation token. - [Obsolete ("Use GetMessageHeaders (IList indexes, CancellationToken cancellationToken) instead.")] - IList GetMessageHeaders (IList uids, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously get the headers for the specified messages. - /// - /// - /// Asynchronously gets the headers for the specified messages. - /// - /// The headers for the specified messages. - /// The UIDs of the message. - /// The cancellation token. - [Obsolete ("Use GetMessageHeadersAsync (IList indexes, CancellationToken cancellationToken) instead.")] - Task> GetMessageHeadersAsync (IList uids, CancellationToken cancellationToken = default (CancellationToken)); + Task GetMessageHeadersAsync (int index, CancellationToken cancellationToken = default); /// /// Get the headers for the specified messages. @@ -279,7 +205,7 @@ public interface IMailSpool : IMailService, IEnumerable /// The headers for the specified messages. /// The indexes of the messages. /// The cancellation token. - IList GetMessageHeaders (IList indexes, CancellationToken cancellationToken = default (CancellationToken)); + IList GetMessageHeaders (IList indexes, CancellationToken cancellationToken = default); /// /// Asynchronously get the headers for the specified messages. @@ -290,7 +216,7 @@ public interface IMailSpool : IMailService, IEnumerable /// The headers for the specified messages. /// The indexes of the messages. /// The cancellation token. - Task> GetMessageHeadersAsync (IList indexes, CancellationToken cancellationToken = default (CancellationToken)); + Task> GetMessageHeadersAsync (IList indexes, CancellationToken cancellationToken = default); /// /// Get the headers of the messages within the specified range. @@ -302,7 +228,7 @@ public interface IMailSpool : IMailService, IEnumerable /// The index of the first message to get. /// The number of messages to get. /// The cancellation token. - IList GetMessageHeaders (int startIndex, int count, CancellationToken cancellationToken = default (CancellationToken)); + IList GetMessageHeaders (int startIndex, int count, CancellationToken cancellationToken = default); /// /// Get the headers of the messages within the specified range. @@ -314,31 +240,7 @@ public interface IMailSpool : IMailService, IEnumerable /// The index of the first message to get. /// The number of messages to get. /// The cancellation token. - Task> GetMessageHeadersAsync (int startIndex, int count, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Get the message with the specified UID. - /// - /// - /// Gets the message with the specified UID. - /// - /// The message. - /// The UID of the message. - /// The cancellation token. - [Obsolete ("Use GetMessage (int index, CancellationToken cancellationToken) instead.")] - MimeMessage GetMessage (string uid, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously get the message with the specified UID. - /// - /// - /// Asynchronously gets the message with the specified UID. - /// - /// The message. - /// The UID of the message. - /// The cancellation token. - [Obsolete ("Use GetMessageAsync (int index, CancellationToken cancellationToken) instead.")] - Task GetMessageAsync (string uid, CancellationToken cancellationToken = default (CancellationToken)); + Task> GetMessageHeadersAsync (int startIndex, int count, CancellationToken cancellationToken = default); /// /// Get the message at the specified index. @@ -350,7 +252,7 @@ public interface IMailSpool : IMailService, IEnumerable /// The index of the message. /// The cancellation token. /// The progress reporting mechanism. - MimeMessage GetMessage (int index, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + MimeMessage GetMessage (int index, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// /// Asynchronously get the message at the specified index. @@ -362,31 +264,7 @@ public interface IMailSpool : IMailService, IEnumerable /// The index of the message. /// The cancellation token. /// The progress reporting mechanism. - Task GetMessageAsync (int index, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Get the messages with the specified UIDs. - /// - /// - /// Gets the messages with the specified UIDs. - /// - /// The messages. - /// The UID of the messages. - /// The cancellation token. - [Obsolete ("Use GetMessages (IList indexes, CancellationToken cancellationToken) instead.")] - IList GetMessages (IList uids, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously get the messages with the specified UIDs. - /// - /// - /// Asynchronously gets the messages with the specified UIDs. - /// - /// The messages. - /// The UIDs of the messages. - /// The cancellation token. - [Obsolete ("Use GetMessagesAsync (IList indexes, CancellationToken cancellationToken) instead.")] - Task> GetMessagesAsync (IList uids, CancellationToken cancellationToken = default (CancellationToken)); + Task GetMessageAsync (int index, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// /// Get the messages at the specified indexes. @@ -398,7 +276,7 @@ public interface IMailSpool : IMailService, IEnumerable /// The indexes of the messages. /// The cancellation token. /// The progress reporting mechanism. - IList GetMessages (IList indexes, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + IList GetMessages (IList indexes, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// /// Asynchronously get the messages at the specified indexes. @@ -410,7 +288,7 @@ public interface IMailSpool : IMailService, IEnumerable /// The indexes of the messages. /// The cancellation token. /// The progress reporting mechanism. - Task> GetMessagesAsync (IList indexes, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + Task> GetMessagesAsync (IList indexes, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// /// Get the messages within the specified range. @@ -423,7 +301,7 @@ public interface IMailSpool : IMailService, IEnumerable /// The number of messages to get. /// The cancellation token. /// The progress reporting mechanism. - IList GetMessages (int startIndex, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + IList GetMessages (int startIndex, int count, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// /// Asynchronously get the messages within the specified range. @@ -436,7 +314,7 @@ public interface IMailSpool : IMailService, IEnumerable /// The number of messages to get. /// The cancellation token. /// The progress reporting mechanism. - Task> GetMessagesAsync (int startIndex, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + Task> GetMessagesAsync (int startIndex, int count, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// /// Get the message or header stream at the specified index. @@ -446,10 +324,10 @@ public interface IMailSpool : IMailService, IEnumerable /// /// The message or header stream. /// The index of the message. - /// true if only the headers should be retrieved; otherwise, false. + /// if only the headers should be retrieved; otherwise, . /// The cancellation token. /// The progress reporting mechanism. - Stream GetStream (int index, bool headersOnly = false, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + Stream GetStream (int index, bool headersOnly = false, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// /// Asynchronously get the message or header stream at the specified index. @@ -459,10 +337,10 @@ public interface IMailSpool : IMailService, IEnumerable /// /// The message or header stream. /// The index of the message. - /// true if only the headers should be retrieved; otherwise, false. + /// if only the headers should be retrieved; otherwise, . /// The cancellation token. /// The progress reporting mechanism. - Task GetStreamAsync (int index, bool headersOnly = false, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + Task GetStreamAsync (int index, bool headersOnly = false, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// /// Get the message or header streams at the specified index. @@ -472,10 +350,10 @@ public interface IMailSpool : IMailService, IEnumerable /// /// The message or header streams. /// The indexes of the messages. - /// true if only the headers should be retrieved; otherwise, false. + /// if only the headers should be retrieved; otherwise, . /// The cancellation token. /// The progress reporting mechanism. - IList GetStreams (IList indexes, bool headersOnly = false, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + IList GetStreams (IList indexes, bool headersOnly = false, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// /// Asynchronously get the message or header streams at the specified indexes. @@ -485,10 +363,10 @@ public interface IMailSpool : IMailService, IEnumerable /// /// The message or header streams. /// The indexes of the messages. - /// true if only the headers should be retrieved; otherwise, false. + /// if only the headers should be retrieved; otherwise, . /// The cancellation token. /// The progress reporting mechanism. - Task> GetStreamsAsync (IList indexes, bool headersOnly = false, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + Task> GetStreamsAsync (IList indexes, bool headersOnly = false, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// /// Get the message or header streams within the specified range. @@ -499,10 +377,10 @@ public interface IMailSpool : IMailService, IEnumerable /// The message or header streams. /// The index of the first stream to get. /// The number of streams to get. - /// true if only the headers should be retrieved; otherwise, false. + /// if only the headers should be retrieved; otherwise, . /// The cancellation token. /// The progress reporting mechanism. - IList GetStreams (int startIndex, int count, bool headersOnly = false, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + IList GetStreams (int startIndex, int count, bool headersOnly = false, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// /// Asynchronously get the message or header streams within the specified range. @@ -513,37 +391,10 @@ public interface IMailSpool : IMailService, IEnumerable /// The messages. /// The index of the first stream to get. /// The number of streams to get. - /// true if only the headers should be retrieved; otherwise, false. + /// if only the headers should be retrieved; otherwise, . /// The cancellation token. /// The progress reporting mechanism. - Task> GetStreamsAsync (int startIndex, int count, bool headersOnly = false, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Mark the specified message for deletion. - /// - /// - /// Messages marked for deletion are not actually deleted until the session - /// is cleanly disconnected - /// (see ). - /// - /// The UID of the message. - /// The cancellation token. - [Obsolete ("Use DeleteMessage (int index, CancellationToken cancellationToken) instead.")] - void DeleteMessage (string uid, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously mark the specified message for deletion. - /// - /// - /// Messages marked for deletion are not actually deleted until the session - /// is cleanly disconnected - /// (see ). - /// - /// An asynchronous task context. - /// The UID of the message. - /// The cancellation token. - [Obsolete ("Use DeleteMessageAsync (int index, CancellationToken cancellationToken) instead.")] - Task DeleteMessageAsync (string uid, CancellationToken cancellationToken = default (CancellationToken)); + Task> GetStreamsAsync (int startIndex, int count, bool headersOnly = false, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// /// Mark the specified message for deletion. @@ -555,7 +406,7 @@ public interface IMailSpool : IMailService, IEnumerable /// /// The index of the message. /// The cancellation token. - void DeleteMessage (int index, CancellationToken cancellationToken = default (CancellationToken)); + void DeleteMessage (int index, CancellationToken cancellationToken = default); /// /// Asynchronously mark the specified message for deletion. @@ -568,34 +419,7 @@ public interface IMailSpool : IMailService, IEnumerable /// An asynchronous task context. /// The index of the message. /// The cancellation token. - Task DeleteMessageAsync (int index, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Mark the specified messages for deletion. - /// - /// - /// Messages marked for deletion are not actually deleted until the session - /// is cleanly disconnected - /// (see ). - /// - /// The UIDs of the messages. - /// The cancellation token. - [Obsolete ("Use DeleteMessages (IList index, CancellationToken cancellationToken) instead.")] - void DeleteMessages (IList uids, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously mark the specified messages for deletion. - /// - /// - /// Messages marked for deletion are not actually deleted until the session - /// is cleanly disconnected - /// (see ). - /// - /// An asynchronous task context. - /// The UIDs of the messages. - /// The cancellation token. - [Obsolete ("Use DeleteMessagesAsync (IList index, CancellationToken cancellationToken) instead.")] - Task DeleteMessagesAsync (IList uids, CancellationToken cancellationToken = default (CancellationToken)); + Task DeleteMessageAsync (int index, CancellationToken cancellationToken = default); /// /// Mark the specified messages for deletion. @@ -607,7 +431,7 @@ public interface IMailSpool : IMailService, IEnumerable /// /// The indexes of the messages. /// The cancellation token. - void DeleteMessages (IList indexes, CancellationToken cancellationToken = default (CancellationToken)); + void DeleteMessages (IList indexes, CancellationToken cancellationToken = default); /// /// Asynchronously mark the specified messages for deletion. @@ -620,7 +444,7 @@ public interface IMailSpool : IMailService, IEnumerable /// An asynchronous task context. /// The indexes of the messages. /// The cancellation token. - Task DeleteMessagesAsync (IList indexes, CancellationToken cancellationToken = default (CancellationToken)); + Task DeleteMessagesAsync (IList indexes, CancellationToken cancellationToken = default); /// /// Mark the specified range of messages for deletion. @@ -633,7 +457,7 @@ public interface IMailSpool : IMailService, IEnumerable /// The index of the first message to mark for deletion. /// The number of messages to mark for deletion. /// The cancellation token. - void DeleteMessages (int startIndex, int count, CancellationToken cancellationToken = default (CancellationToken)); + void DeleteMessages (int startIndex, int count, CancellationToken cancellationToken = default); /// /// Asynchronously mark the specified range of messages for deletion. @@ -647,7 +471,7 @@ public interface IMailSpool : IMailService, IEnumerable /// The index of the first message to mark for deletion. /// The number of messages to mark for deletion. /// The cancellation token. - Task DeleteMessagesAsync (int startIndex, int count, CancellationToken cancellationToken = default (CancellationToken)); + Task DeleteMessagesAsync (int startIndex, int count, CancellationToken cancellationToken = default); /// /// Mark all messages for deletion. @@ -658,7 +482,7 @@ public interface IMailSpool : IMailService, IEnumerable /// (see ). /// /// The cancellation token. - void DeleteAllMessages (CancellationToken cancellationToken = default (CancellationToken)); + void DeleteAllMessages (CancellationToken cancellationToken = default); /// /// Asynchronously mark all messages for deletion. @@ -670,7 +494,7 @@ public interface IMailSpool : IMailService, IEnumerable /// /// An asynchronous task context. /// The cancellation token. - Task DeleteAllMessagesAsync (CancellationToken cancellationToken = default (CancellationToken)); + Task DeleteAllMessagesAsync (CancellationToken cancellationToken = default); /// /// Reset the state of all messages marked for deletion. @@ -681,7 +505,7 @@ public interface IMailSpool : IMailService, IEnumerable /// (see ). /// /// The cancellation token. - void Reset (CancellationToken cancellationToken = default (CancellationToken)); + void Reset (CancellationToken cancellationToken = default); /// /// Asynchronously reset the state of all messages marked for deletion. @@ -693,6 +517,6 @@ public interface IMailSpool : IMailService, IEnumerable /// /// An asynchronous task context. /// The cancellation token. - Task ResetAsync (CancellationToken cancellationToken = default (CancellationToken)); + Task ResetAsync (CancellationToken cancellationToken = default); } } diff --git a/MailKit/IMailStore.cs b/MailKit/IMailStore.cs index 7562553dd2..1ee9204b9b 100644 --- a/MailKit/IMailStore.cs +++ b/MailKit/IMailStore.cs @@ -1,9 +1,9 @@ -// +// // IMailStore.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -31,10 +31,11 @@ namespace MailKit { /// - /// An interface for retreiving messages from a message store such as IMAP. + /// An interface for retrieving messages from a message store. /// /// - /// Implemented by . + /// An interface for retrieving messages from a message store. + /// Implemented by . /// public interface IMailStore : IMailService { @@ -71,9 +72,23 @@ public interface IMailStore : IMailService /// /// Gets whether or not the mail store supports quotas. /// - /// true if the mail store supports quotas; otherwise, false. + /// if the mail store supports quotas; otherwise, . bool SupportsQuotas { get; } + /// + /// Get the threading algorithms supported by the mail store. + /// + /// + /// The threading algorithms are queried as part of the + /// Connect + /// and Authenticate methods. + /// + /// + /// + /// + /// The threading algorithms. + HashSet ThreadingAlgorithms { get; } + /// /// Get the Inbox folder. /// @@ -125,7 +140,7 @@ public interface IMailStore : IMailService /// /// The command failed. /// - void EnableQuickResync (CancellationToken cancellationToken = default (CancellationToken)); + void EnableQuickResync (CancellationToken cancellationToken = default); /// /// Asynchronously enable the quick resynchronization feature. @@ -169,21 +184,21 @@ public interface IMailStore : IMailService /// /// The command failed. /// - Task EnableQuickResyncAsync (CancellationToken cancellationToken = default (CancellationToken)); + Task EnableQuickResyncAsync (CancellationToken cancellationToken = default); /// /// Get the specified special folder. /// /// /// Not all message stores support the concept of special folders, - /// so this method may return null. + /// so this method may return . /// - /// The folder if available; otherwise null. + /// The folder if available; otherwise . /// The type of special folder. /// /// is out of range. /// - IMailFolder GetFolder (SpecialFolder folder); + IMailFolder? GetFolder (SpecialFolder folder); /// /// Get the folder for the specified namespace. @@ -195,7 +210,7 @@ public interface IMailStore : IMailService /// The folder. /// The namespace. /// - /// is null. + /// is . /// /// /// The folder could not be found. @@ -210,10 +225,10 @@ public interface IMailStore : IMailService /// /// The folders. /// The namespace. - /// If set to true, only subscribed folders will be listed. + /// If set to , only subscribed folders will be listed. /// The cancellation token. /// - /// is null. + /// is . /// /// /// The has been disposed. @@ -239,7 +254,7 @@ public interface IMailStore : IMailService /// /// The command failed. /// - IList GetFolders (FolderNamespace @namespace, bool subscribedOnly, CancellationToken cancellationToken = default (CancellationToken)); + IList GetFolders (FolderNamespace @namespace, bool subscribedOnly, CancellationToken cancellationToken = default); /// /// Asynchronously get all of the folders within the specified namespace. @@ -249,10 +264,10 @@ public interface IMailStore : IMailService /// /// The folders. /// The namespace. - /// If set to true, only subscribed folders will be listed. + /// If set to , only subscribed folders will be listed. /// The cancellation token. /// - /// is null. + /// is . /// /// /// The has been disposed. @@ -278,7 +293,7 @@ public interface IMailStore : IMailService /// /// The command failed. /// - Task> GetFoldersAsync (FolderNamespace @namespace, bool subscribedOnly, CancellationToken cancellationToken = default (CancellationToken)); + Task> GetFoldersAsync (FolderNamespace @namespace, bool subscribedOnly, CancellationToken cancellationToken = default); /// /// Get all of the folders within the specified namespace. @@ -289,10 +304,10 @@ public interface IMailStore : IMailService /// The folders. /// The namespace. /// The status items to pre-populate. - /// If set to true, only subscribed folders will be listed. + /// If set to , only subscribed folders will be listed. /// The cancellation token. /// - /// is null. + /// is . /// /// /// The has been disposed. @@ -318,7 +333,7 @@ public interface IMailStore : IMailService /// /// The command failed. /// - IList GetFolders (FolderNamespace @namespace, StatusItems items = StatusItems.None, bool subscribedOnly = false, CancellationToken cancellationToken = default (CancellationToken)); + IList GetFolders (FolderNamespace @namespace, StatusItems items = StatusItems.None, bool subscribedOnly = false, CancellationToken cancellationToken = default); /// /// Asynchronously get all of the folders within the specified namespace. @@ -329,10 +344,10 @@ public interface IMailStore : IMailService /// The folders. /// The namespace. /// The status items to pre-populate. - /// If set to true, only subscribed folders will be listed. + /// If set to , only subscribed folders will be listed. /// The cancellation token. /// - /// is null. + /// is . /// /// /// The has been disposed. @@ -358,7 +373,7 @@ public interface IMailStore : IMailService /// /// The command failed. /// - Task> GetFoldersAsync (FolderNamespace @namespace, StatusItems items = StatusItems.None, bool subscribedOnly = false, CancellationToken cancellationToken = default (CancellationToken)); + Task> GetFoldersAsync (FolderNamespace @namespace, StatusItems items = StatusItems.None, bool subscribedOnly = false, CancellationToken cancellationToken = default); /// /// Get the folder for the specified path. @@ -370,7 +385,7 @@ public interface IMailStore : IMailService /// The folder path. /// The cancellation token. /// - /// is null. + /// is . /// /// /// The operation was canceled via the cancellation token. @@ -387,7 +402,7 @@ public interface IMailStore : IMailService /// /// The command failed. /// - IMailFolder GetFolder (string path, CancellationToken cancellationToken = default (CancellationToken)); + IMailFolder GetFolder (string path, CancellationToken cancellationToken = default); /// /// Asynchronously get the folder for the specified path. @@ -399,7 +414,7 @@ public interface IMailStore : IMailService /// The folder path. /// The cancellation token. /// - /// is null. + /// is . /// /// /// The operation was canceled via the cancellation token. @@ -416,7 +431,7 @@ public interface IMailStore : IMailService /// /// The command failed. /// - Task GetFolderAsync (string path, CancellationToken cancellationToken = default (CancellationToken)); + Task GetFolderAsync (string path, CancellationToken cancellationToken = default); /// /// Gets the specified metadata. @@ -427,7 +442,7 @@ public interface IMailStore : IMailService /// The requested metadata value. /// The metadata tag. /// The cancellation token. - string GetMetadata (MetadataTag tag, CancellationToken cancellationToken = default (CancellationToken)); + string? GetMetadata (MetadataTag tag, CancellationToken cancellationToken = default); /// /// Asynchronously gets the specified metadata. @@ -438,7 +453,7 @@ public interface IMailStore : IMailService /// The requested metadata value. /// The metadata tag. /// The cancellation token. - Task GetMetadataAsync (MetadataTag tag, CancellationToken cancellationToken = default (CancellationToken)); + Task GetMetadataAsync (MetadataTag tag, CancellationToken cancellationToken = default); /// /// Gets the specified metadata. @@ -449,7 +464,7 @@ public interface IMailStore : IMailService /// The requested metadata. /// The metadata tags. /// The cancellation token. - MetadataCollection GetMetadata (IEnumerable tags, CancellationToken cancellationToken = default (CancellationToken)); + MetadataCollection GetMetadata (IEnumerable tags, CancellationToken cancellationToken = default); /// /// Asynchronously gets the specified metadata. @@ -460,7 +475,7 @@ public interface IMailStore : IMailService /// The requested metadata. /// The metadata tags. /// The cancellation token. - Task GetMetadataAsync (IEnumerable tags, CancellationToken cancellationToken = default (CancellationToken)); + Task GetMetadataAsync (IEnumerable tags, CancellationToken cancellationToken = default); /// /// Gets the specified metadata. @@ -472,7 +487,7 @@ public interface IMailStore : IMailService /// The metadata options. /// The metadata tags. /// The cancellation token. - MetadataCollection GetMetadata (MetadataOptions options, IEnumerable tags, CancellationToken cancellationToken = default (CancellationToken)); + MetadataCollection GetMetadata (MetadataOptions options, IEnumerable tags, CancellationToken cancellationToken = default); /// /// Asynchronously gets the specified metadata. @@ -484,7 +499,7 @@ public interface IMailStore : IMailService /// The metadata options. /// The metadata tags. /// The cancellation token. - Task GetMetadataAsync (MetadataOptions options, IEnumerable tags, CancellationToken cancellationToken = default (CancellationToken)); + Task GetMetadataAsync (MetadataOptions options, IEnumerable tags, CancellationToken cancellationToken = default); /// /// Sets the specified metadata. @@ -494,7 +509,7 @@ public interface IMailStore : IMailService /// /// The metadata. /// The cancellation token. - void SetMetadata (MetadataCollection metadata, CancellationToken cancellationToken = default (CancellationToken)); + void SetMetadata (MetadataCollection metadata, CancellationToken cancellationToken = default); /// /// Asynchronously sets the specified metadata. @@ -505,7 +520,7 @@ public interface IMailStore : IMailService /// An asynchronous task context. /// The metadata. /// The cancellation token. - Task SetMetadataAsync (MetadataCollection metadata, CancellationToken cancellationToken = default (CancellationToken)); + Task SetMetadataAsync (MetadataCollection metadata, CancellationToken cancellationToken = default); /// /// Occurs when a remote message store receives an alert message from the server. @@ -515,5 +530,21 @@ public interface IMailStore : IMailService /// will emit Alert events when they receive alert messages from the server. /// event EventHandler Alert; + + /// + /// Occurs when a folder is created. + /// + /// + /// The event is emitted when a new folder is created. + /// + event EventHandler FolderCreated; + + /// + /// Occurs when metadata changes. + /// + /// + /// The event is emitted when metadata changes. + /// + event EventHandler MetadataChanged; } } diff --git a/MailKit/IMailTransport.cs b/MailKit/IMailTransport.cs index de65e4d13d..e96e464dcc 100644 --- a/MailKit/IMailTransport.cs +++ b/MailKit/IMailTransport.cs @@ -1,9 +1,9 @@ -// +// // IMailTransport.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -36,7 +36,8 @@ namespace MailKit { /// An interface for sending messages. /// /// - /// An interface for sending messages. + /// An interface for sending messages. + /// Implemented by . /// public interface IMailTransport : IMailService { @@ -52,10 +53,11 @@ public interface IMailTransport : IMailService /// the recipients are collected from the Resent-To, Resent-Cc, and /// Resent-Bcc headers, otherwise the To, Cc, and Bcc headers are used. /// + /// The final free-form text response from the server. /// The message. /// The cancellation token. /// The progress reporting mechanism. - void Send (MimeMessage message, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + string Send (MimeMessage message, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// /// Asynchronously send the specified message. @@ -69,11 +71,11 @@ public interface IMailTransport : IMailService /// the recipients are collected from the Resent-To, Resent-Cc, and /// Resent-Bcc headers, otherwise the To, Cc, and Bcc headers are used. /// - /// An asynchronous task context. + /// The final free-form text response from the server. /// The message. /// The cancellation token. /// The progress reporting mechanism. - Task SendAsync (MimeMessage message, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + Task SendAsync (MimeMessage message, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// /// Send the specified message using the supplied sender and recipients. @@ -81,12 +83,13 @@ public interface IMailTransport : IMailService /// /// Sends the specified message using the supplied sender and recipients. /// + /// The final free-form text response from the server. /// The message. /// The mailbox address to use for sending the message. /// The mailbox addresses that should receive the message. /// The cancellation token. /// The progress reporting mechanism. - void Send (MimeMessage message, MailboxAddress sender, IEnumerable recipients, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + string Send (MimeMessage message, MailboxAddress sender, IEnumerable recipients, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// /// Asynchronously send the specified message using the supplied sender and recipients. @@ -94,13 +97,13 @@ public interface IMailTransport : IMailService /// /// Asynchronously sends the specified message using the supplied sender and recipients. /// - /// An asynchronous task context. + /// The final free-form text response from the server. /// The message. /// The mailbox address to use for sending the message. /// The mailbox addresses that should receive the message. /// The cancellation token. /// The progress reporting mechanism. - Task SendAsync (MimeMessage message, MailboxAddress sender, IEnumerable recipients, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + Task SendAsync (MimeMessage message, MailboxAddress sender, IEnumerable recipients, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// /// Send the specified message. @@ -114,11 +117,12 @@ public interface IMailTransport : IMailService /// the recipients are collected from the Resent-To, Resent-Cc, and /// Resent-Bcc headers, otherwise the To, Cc, and Bcc headers are used. /// + /// The final free-form text response from the server. /// The formatting options. /// The message. /// The cancellation token. /// The progress reporting mechanism. - void Send (FormatOptions options, MimeMessage message, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + string Send (FormatOptions options, MimeMessage message, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// /// Asynchronously send the specified message. @@ -132,12 +136,12 @@ public interface IMailTransport : IMailService /// the recipients are collected from the Resent-To, Resent-Cc, and /// Resent-Bcc headers, otherwise the To, Cc, and Bcc headers are used. /// - /// An asynchronous task context. + /// The final free-form text response from the server. /// The formatting options. /// The message. /// The cancellation token. /// The progress reporting mechanism. - Task SendAsync (FormatOptions options, MimeMessage message, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + Task SendAsync (FormatOptions options, MimeMessage message, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// /// Send the specified message using the supplied sender and recipients. @@ -145,13 +149,14 @@ public interface IMailTransport : IMailService /// /// Sends the specified message using the supplied sender and recipients. /// + /// The final free-form text response from the server. /// The formatting options. /// The message. /// The mailbox address to use for sending the message. /// The mailbox addresses that should receive the message. /// The cancellation token. /// The progress reporting mechanism. - void Send (FormatOptions options, MimeMessage message, MailboxAddress sender, IEnumerable recipients, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + string Send (FormatOptions options, MimeMessage message, MailboxAddress sender, IEnumerable recipients, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// /// Asynchronously send the specified message using the supplied sender and recipients. @@ -159,14 +164,14 @@ public interface IMailTransport : IMailService /// /// Asynchronously sends the specified message using the supplied sender and recipients. /// - /// An asynchronous task context. + /// The final free-form text response from the server. /// The formatting options. /// The message. /// The mailbox address to use for sending the message. /// The mailbox addresses that should receive the message. /// The cancellation token. /// The progress reporting mechanism. - Task SendAsync (FormatOptions options, MimeMessage message, MailboxAddress sender, IEnumerable recipients, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + Task SendAsync (FormatOptions options, MimeMessage message, MailboxAddress sender, IEnumerable recipients, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// /// Occurs when a message is successfully sent via the transport. @@ -174,6 +179,6 @@ public interface IMailTransport : IMailService /// /// The event will be emitted each time a message is successfully sent. /// - event EventHandler MessageSent; + event EventHandler? MessageSent; } } diff --git a/MailKit/IMessageSummary.cs b/MailKit/IMessageSummary.cs index 7828ce81e5..5aa9969102 100644 --- a/MailKit/IMessageSummary.cs +++ b/MailKit/IMessageSummary.cs @@ -1,9 +1,9 @@ -// +// // IMessageSummary.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -29,18 +29,36 @@ using MimeKit; +#if NET5_0_OR_GREATER +using IReadOnlySetOfStrings = System.Collections.Generic.IReadOnlySet; +#else +using IReadOnlySetOfStrings = System.Collections.Generic.ISet; +#endif + namespace MailKit { /// /// A summary of a message. /// /// - /// A is returned by - /// . - /// The properties of the that will be available - /// depend on the passed to the aformentioned method. + /// The Fetch and + /// FetchAsync methods + /// return lists of items. + /// The properties of the that will be available + /// depend on the passed to the aforementioned method. /// public interface IMessageSummary { + /// + /// Get the folder that the message belongs to. + /// + /// + /// Gets the folder that the message belongs to, if available. + /// + /// The folder. + IMailFolder? Folder { + get; + } + /// /// Get a bitmask of fields that have been populated. /// @@ -57,39 +75,49 @@ public interface IMessageSummary /// The body will be one of , /// , , /// or . - /// This property will only be set if the - /// flag is passed to - /// . + /// This property will only be set if either the + /// flag or the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. /// /// The body structure of the message. - BodyPart Body { get; } + BodyPart? Body { get; } /// /// Gets the text body part of the message if it exists. /// /// /// Gets the text/plain body part of the message. - /// In order for this to work properly, it is necessary to include - /// when fetching - /// summary information from a . + /// This property will only be usable if the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. /// /// - /// + /// /// - /// The text body if it exists; otherwise, null. - BodyPartText TextBody { get; } + /// The text body if it exists; otherwise, . + BodyPartText? TextBody { get; } /// /// Gets the html body part of the message if it exists. /// /// /// Gets the text/html body part of the message. - /// In order for this to work properly, it is necessary to include - /// when fetching - /// summary information from a . + /// This property will only be usable if the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. /// - /// The html body if it exists; otherwise, null. - BodyPartText HtmlBody { get; } + /// + /// + /// + /// The html body if it exists; otherwise, . + BodyPartText? HtmlBody { get; } /// /// Gets the body parts of the message. @@ -97,10 +125,12 @@ public interface IMessageSummary /// /// Traverses over the , enumerating all of the /// objects. - /// In order for this to work, it is necessary to include - /// or - /// when fetching - /// summary information from a . + /// This property will only be usable if either the + /// flag or the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. /// /// The body parts. IEnumerable BodyParts { get; } @@ -112,16 +142,34 @@ public interface IMessageSummary /// Traverses over the , enumerating all of the /// objects that have a Content-Disposition /// header set to "attachment". - /// In order for this to work properly, it is necessary to include - /// when fetching - /// summary information from a . + /// This property will only be usable if the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. /// /// - /// + /// /// /// The attachments. IEnumerable Attachments { get; } + /// + /// Gets the preview text of the message. + /// + /// + /// The preview text is a short snippet of the beginning of the message + /// text, typically shown in a mail client's message list to provide the user + /// with a sense of what the message is about. + /// This property will only be set if the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. + /// + /// The preview text. + string? PreviewText { get; } + /// /// Gets the envelope of the message, if available. /// @@ -133,10 +181,12 @@ public interface IMessageSummary /// and the message id. /// This property will only be set if the /// flag is passed to - /// . + /// one of the Fetch + /// or FetchAsync + /// methods. /// /// The envelope of the message. - Envelope Envelope { get; } + Envelope? Envelope { get; } /// /// Gets the normalized subject. @@ -164,7 +214,7 @@ public interface IMessageSummary /// /// This value should be based on whether the message subject contained any "Re:", "Re[#]:" or "FWD:" prefixes. /// - /// true if the message is a reply; otherwise, false. + /// if the message is a reply; otherwise, . bool IsReply { get; } /// @@ -174,7 +224,9 @@ public interface IMessageSummary /// Gets the message flags, if available. /// This property will only be set if the /// flag is passed to - /// . + /// one of the Fetch + /// or FetchAsync + /// methods. /// /// The message flags. MessageFlags? Flags { get; } @@ -186,22 +238,40 @@ public interface IMessageSummary /// Gets the user-defined message flags, if available. /// This property will only be set if the /// flag is passed to - /// . + /// one of the Fetch + /// or FetchAsync + /// methods. /// /// The user-defined message flags. - HashSet UserFlags { get; } + IReadOnlySetOfStrings Keywords { get; } + + /// + /// Gets the message annotations, if available. + /// + /// + /// Gets the message annotations, if available. + /// This property will only be set if the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. + /// + /// The message annotations. + IReadOnlyList? Annotations { get; } /// /// Gets the list of headers, if available. /// /// /// Gets the list of headers, if available. - /// This property will only be set if the - /// . - /// method is used. + /// This property will only be set if the used with + /// Fetch or + /// FetchAsync has the + /// flag set on or if the list is non-empty. + /// /// /// The list of headers. - HeaderList Headers { get; } + HeaderList? Headers { get; } /// /// Gets the internal date of the message, if available. @@ -210,11 +280,27 @@ public interface IMessageSummary /// Gets the internal date of the message (often the same date as found in the Received header), if available. /// This property will only be set if the /// flag is passed to - /// . + /// one of the Fetch + /// or FetchAsync + /// methods. /// /// The internal date of the message. DateTimeOffset? InternalDate { get; } + /// + /// Gets the date and time that the message was saved to the current mailbox, if available. + /// + /// + /// Gets the date and time that the message was saved to the current mailbox, if available. + /// This property will only be set if the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. + /// + /// The save date of the message. + DateTimeOffset? SaveDate { get; } + /// /// Gets the size of the message, in bytes, if available. /// @@ -222,7 +308,9 @@ public interface IMessageSummary /// Gets the size of the message, in bytes, if available. /// This property will only be set if the /// flag is passed to - /// . + /// one of the Fetch + /// or FetchAsync + /// methods. /// /// The size of the message. uint? Size { get; } @@ -234,7 +322,9 @@ public interface IMessageSummary /// Gets the mod-sequence value for the message, if available. /// This property will only be set if the /// flag is passed to - /// . + /// one of the Fetch + /// or FetchAsync + /// methods. /// /// The mod-sequence value. ulong? ModSeq { get; } @@ -246,10 +336,44 @@ public interface IMessageSummary /// Gets the message-ids that the message references, if available. /// This property will only be set if the /// flag is passed to - /// . + /// one of the Fetch + /// or FetchAsync + /// methods. /// /// The references. - MessageIdList References { get; } + MessageIdList? References { get; } + + /// + /// Get the globally unique identifier for the message, if available. + /// + /// + /// Gets the globally unique identifier of the message, if available. + /// This property will only be set if the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. + /// This property maps to the EMAILID value defined in the + /// OBJECTID extension. + /// + /// The globally unique message identifier. + string? EmailId { get; } + + /// + /// Get the globally unique thread identifier for the message, if available. + /// + /// + /// Gets the globally unique thread identifier for the message, if available. + /// This property will only be set if the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. + /// This property maps to the THREADID value defined in the + /// OBJECTID extension. + /// + /// The globally unique thread identifier. + string? ThreadId { get; } /// /// Gets the unique identifier of the message, if available. @@ -258,7 +382,9 @@ public interface IMessageSummary /// Gets the unique identifier of the message, if available. /// This property will only be set if the /// flag is passed to - /// . + /// one of the Fetch + /// or FetchAsync + /// methods. /// /// The uid of the message. UniqueId UniqueId { get; } @@ -267,7 +393,8 @@ public interface IMessageSummary /// Gets the index of the message. /// /// - /// Gets the index of the message. + /// Gets the index of the message. + /// This property is always set. /// /// The index of the message. int Index { get; } @@ -281,7 +408,9 @@ public interface IMessageSummary /// Gets the GMail message identifier, if available. /// This property will only be set if the /// flag is passed to - /// . + /// one of the Fetch + /// or FetchAsync + /// methods. /// /// The GMail message identifier. ulong? GMailMessageId { get; } @@ -293,7 +422,9 @@ public interface IMessageSummary /// Gets the GMail thread identifier, if available. /// This property will only be set if the /// flag is passed to - /// . + /// one of the Fetch + /// or FetchAsync + /// methods. /// /// The GMail thread identifier. ulong? GMailThreadId { get; } @@ -305,10 +436,12 @@ public interface IMessageSummary /// Gets the list of GMail labels, if available. /// This property will only be set if the /// flag is passed to - /// . + /// one of the Fetch + /// or FetchAsync + /// methods. /// /// The GMail labels. - IList GMailLabels { get; } + IList? GMailLabels { get; } #endregion } diff --git a/MailKit/IProtocolLogger.cs b/MailKit/IProtocolLogger.cs index 8edc399ae8..8e7782f0fb 100644 --- a/MailKit/IProtocolLogger.cs +++ b/MailKit/IProtocolLogger.cs @@ -1,9 +1,9 @@ -// +// // IProtocolLogger.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -28,16 +28,25 @@ namespace MailKit { /// - /// An interface for logging protocols. + /// An interface for logging the communication between a client and server. /// /// - /// An interface for logging protocols. + /// An interface for logging the communication between a client and server. /// /// - /// + /// /// public interface IProtocolLogger : IDisposable { + /// + /// Get or set the authentication secret detector. + /// + /// + /// Gets or sets the authentication secret detector. + /// + /// The authentication secret detector. + IAuthenticationSecretDetector? AuthenticationSecretDetector { get; set; } + /// /// Logs a connection to the specified URI. /// @@ -46,7 +55,7 @@ public interface IProtocolLogger : IDisposable /// /// The URI. /// - /// is null. + /// is . /// /// /// The logger has been disposed. @@ -60,18 +69,21 @@ public interface IProtocolLogger : IDisposable /// Logs a sequence of bytes sent by the client. /// /// - /// Logs a sequence of bytes sent by the client. + /// Logs a sequence of bytes sent by the client. + /// is called by the upon every successful + /// write operation to its underlying network stream, passing the exact same , + /// , and arguments to the logging function. /// /// The buffer to log. /// The offset of the first byte to log. /// The number of bytes to log. /// - /// is null. + /// is . /// /// /// is less than zero or greater than the length of . /// -or- - /// The is not large enough to contain bytes strting + /// The is not large enough to contain bytes starting /// at the specified . /// /// @@ -86,18 +98,20 @@ public interface IProtocolLogger : IDisposable /// Logs a sequence of bytes sent by the server. /// /// - /// Logs a sequence of bytes sent by the server. + /// Logs a sequence of bytes sent by the server. + /// is called by the upon every successful + /// read of its underlying network stream with the exact buffer that was read. /// /// The buffer to log. /// The offset of the first byte to log. /// The number of bytes to log. /// - /// is null. + /// is . /// /// /// is less than zero or greater than the length of . /// -or- - /// The is not large enough to contain bytes strting + /// The is not large enough to contain bytes starting /// at the specified . /// /// diff --git a/MailKit/IReplaceRequest.cs b/MailKit/IReplaceRequest.cs new file mode 100644 index 0000000000..d96f5aaf55 --- /dev/null +++ b/MailKit/IReplaceRequest.cs @@ -0,0 +1,47 @@ +// +// IReplaceRequest.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +namespace MailKit { + /// + /// A request for replacing a message in a folder. + /// + /// + /// A request for replacing a message in a folder. + /// + public interface IReplaceRequest : IAppendRequest + { + /// + /// Get or set the folder where the replacement message should be appended. + /// + /// + /// Gets or sets the folder where the replacement message should be appended. + /// If no destination folder is specified, then the replacement message will be + /// appended to the original folder. + /// + /// The destination folder. + IMailFolder? Destination { get; set; } + } +} diff --git a/MailKit/IStoreFlagsRequest.cs b/MailKit/IStoreFlagsRequest.cs new file mode 100644 index 0000000000..b41d123b1b --- /dev/null +++ b/MailKit/IStoreFlagsRequest.cs @@ -0,0 +1,76 @@ +// +// IStoreFlagsRequest.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System.Collections.Generic; + +namespace MailKit { + /// + /// A request for storing message flags. + /// + /// + /// A request for storing message flags. + /// + public interface IStoreFlagsRequest : IStoreRequest + { + /// + /// Get the store action to perform. + /// + /// + /// Gets the store action to perform. + /// + /// The store action. + StoreAction Action { get; } + + /// + /// Get or set the message flags that should be added, removed, or set. + /// + /// + /// Gets or sets the message flags that should be added, removed, or set. + /// + /// The message flags. + MessageFlags Flags { get; set; } + + /// + /// Get the keywords that should be added, removed, or set. + /// + /// + /// Gets the keywords that should be added, removed, or set. + /// + /// The keywords. + ISet Keywords { get; } + + /// + /// Get or set whether the store operation should run silently. + /// + /// + /// Gets or sets whether the store operation should run silently. + /// Normally, when flags or keywords are changed on a message, a event is emitted. + /// By setting to , this event will not be emitted as a result of this store operation. + /// + /// if the store operation should run silently (not emitting events for flag changes); otherwise, . + bool Silent { get; set; } + } +} diff --git a/MailKit/IStoreLabelsRequest.cs b/MailKit/IStoreLabelsRequest.cs new file mode 100644 index 0000000000..7978388ed9 --- /dev/null +++ b/MailKit/IStoreLabelsRequest.cs @@ -0,0 +1,67 @@ +// +// IStoreLabelsRequest.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System.Collections.Generic; + +namespace MailKit { + /// + /// A request for storing GMail-style labels for a message. + /// + /// + /// A request for storing GMail-style labels for a message. + /// + public interface IStoreLabelsRequest : IStoreRequest + { + /// + /// Get the store action to perform. + /// + /// + /// Gets the store action to perform. + /// + /// The store action. + StoreAction Action { get; } + + /// + /// Get the GMail-style labels that should be added, removed, or set. + /// + /// + /// Gets the GMail-style labels that should be added, removed, or set. + /// + /// The GMail-style labels. + ISet Labels { get; } + + /// + /// Get or set whether the store operation should run silently. + /// + /// + /// Gets or sets whether the store operation should run silently. + /// Normally, when flags or keywords are changed on a message, a event is emitted. + /// By setting to , this event will not be emitted as a result of this store operation. + /// + /// if the store operation should run silently (not emitting events for label changes); otherwise, . + bool Silent { get; set; } + } +} diff --git a/MailKit/IStoreRequest.cs b/MailKit/IStoreRequest.cs new file mode 100644 index 0000000000..5c9ea5261f --- /dev/null +++ b/MailKit/IStoreRequest.cs @@ -0,0 +1,48 @@ +// +// IStoreRequest.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +namespace MailKit { + /// + /// A request for storing flags, labels, or annotations for a message. + /// + /// + /// A request for storing flags, labels, or annotations for a message. + /// + public interface IStoreRequest + { + /// + /// Get or set the mod-sequence value that indicates the last known state of the message(s) being updated. + /// + /// + /// Gets or sets the mod-sequence value that indicates the last known state of the message(s) being updated. + /// If this property is set, only messages that have not had their flags modified since the specified mod-sequence + /// state will have their flags updated by the Store + /// or StoreAsync methods. + /// + /// The mod-sequence value that indicates the last known state of the message(s) being updated. + ulong? UnchangedSince { get; set; } + } +} diff --git a/MailKit/ITransferProgress.cs b/MailKit/ITransferProgress.cs index 7cebfd5091..c178a8306b 100644 --- a/MailKit/ITransferProgress.cs +++ b/MailKit/ITransferProgress.cs @@ -1,9 +1,9 @@ -// +// // ITransferProgress.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal diff --git a/MailKit/MailFolder.cs b/MailKit/MailFolder.cs index 2209c58f50..6e884d9411 100644 --- a/MailKit/MailFolder.cs +++ b/MailKit/MailFolder.cs @@ -1,9 +1,9 @@ -// +// // MailFolder.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -35,6 +35,12 @@ using MailKit.Search; +#if NET5_0_OR_GREATER +using IReadOnlySetOfStrings = System.Collections.Generic.IReadOnlySet; +#else +using IReadOnlySetOfStrings = System.Collections.Generic.ISet; +#endif + namespace MailKit { /// /// An abstract mail folder implementation. @@ -53,25 +59,66 @@ public abstract class MailFolder : IMailFolder protected static readonly MessageFlags SettableFlags = MessageFlags.Answered | MessageFlags.Deleted | MessageFlags.Draft | MessageFlags.Flagged | MessageFlags.Seen; - IMailFolder parent; + IMailFolder? parent; /// - /// Initializes a new instance of the class. + /// Initialize a new instance of the class. /// /// /// Initializes a new instance of the class. /// + [Obsolete ("Use MailFolder (string fullName, char directorySeparator, FolderAttributes attributes) instead.")] protected MailFolder () { + PermanentKeywords = new HashSet (StringComparer.Ordinal); + AcceptedKeywords = new HashSet (StringComparer.Ordinal); + FullName = string.Empty; + Name = string.Empty; + FirstUnread = -1; + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The full name (path) of the folder. + /// The directory separator used by the folder. + /// The attributes of the folder. + /// + /// is . + /// + protected MailFolder (string fullName, char directorySeparator, FolderAttributes attributes) + { + if (fullName == null) + throw new ArgumentNullException (nameof (fullName)); + + PermanentKeywords = new HashSet (StringComparer.Ordinal); + AcceptedKeywords = new HashSet (StringComparer.Ordinal); + Name = GetBaseName (fullName, directorySeparator); + DirectorySeparator = directorySeparator; + Attributes = attributes; + FullName = fullName; + + FirstUnread = -1; + } + + internal static string GetBaseName (string fullName, char delim) + { + int index; + + if ((index = fullName.LastIndexOf (delim)) != -1) + return fullName.Substring (index + 1); + + return fullName; } /// - /// Gets an object that can be used to synchronize access to the folder. + /// Get an object that can be used to synchronize access to the folder. /// /// /// Gets an object that can be used to synchronize access to the folder. - /// When using the non-Async methods from multiple threads, it is important to lock the - /// object for thread safety when using the synchronous methods. /// /// The sync root. public abstract object SyncRoot { @@ -85,10 +132,10 @@ public abstract object SyncRoot { /// Root-level folders do not have a parent folder. /// /// The parent folder. - public IMailFolder ParentFolder { + public IMailFolder? ParentFolder { get { return parent; } internal protected set { - if (value == parent) + if (object.ReferenceEquals (value, parent)) return; if (parent != null) @@ -112,17 +159,68 @@ public FolderAttributes Attributes { get; internal protected set; } + /// + /// Get the annotation access level. + /// + /// + /// If annotations are supported, this property can be used to determine whether or not + /// the supports reading and writing annotations. + /// + /// The annotation access level. + public AnnotationAccess AnnotationAccess { + get; internal protected set; + } + + /// + /// Get the supported annotation scopes. + /// + /// + /// If annotations are supported, this property can be used to determine which + /// annotation scopes are supported by the . + /// + /// The supported annotation scopes. + public AnnotationScope AnnotationScopes { + get; internal protected set; + } + + /// + /// Get the maximum size of annotation values supported by the folder. + /// + /// + /// If annotations are supported, this property can be used to determine the + /// maximum size of annotation values supported by the . + /// + /// The maximum size of annotation values supported by the folder. + public uint MaxAnnotationSize { + get; internal protected set; + } + /// /// Get the permanent flags. /// /// - /// The permanent flags are the message flags that will persist between sessions. + /// The permanent flags are the message flags that will persist between sessions. + /// If the flag is set, then the folder allows + /// storing of user-defined keywords. /// /// The permanent flags. public MessageFlags PermanentFlags { get; protected set; } + /// + /// Get the permanent keywords. + /// + /// + /// The permanent keywords are the keywords that will persist between sessions. + /// If the flag is set in , + /// then the folder allows storing of user-defined keywords as well. + /// + /// The permanent keywords. + public IReadOnlySetOfStrings PermanentKeywords { + get; protected set; + } + /// /// Get the accepted flags. /// @@ -136,6 +234,19 @@ public MessageFlags AcceptedFlags { get; protected set; } + /// + /// Get the accepted keywords. + /// + /// + /// The accepted keywords are the keywords that will be accepted and persist + /// for the current session. For the set of keywords that will persist between + /// sessions, see the property. + /// + /// The accepted keywords. + public IReadOnlySetOfStrings AcceptedKeywords { + get; protected set; + } + /// /// Get the directory separator. /// @@ -155,7 +266,7 @@ public char DirectorySeparator { /// /// The read/write access. public FolderAccess Access { - get; protected set; + get; internal protected set; } /// @@ -164,7 +275,7 @@ public FolderAccess Access { /// /// Gets whether or not the folder is a namespace folder. /// - /// true if the folder is a namespace folder; otherwise, false. + /// if the folder is a namespace folder; otherwise, . public bool IsNamespace { get; protected set; } @@ -191,13 +302,28 @@ public string Name { get; protected set; } + /// + /// Get the unique identifier for the folder, if available. + /// + /// + /// Gets a unique identifier for the folder, if available. This is useful for clients + /// implementing a message cache that want to track the folder after it is renamed by another + /// client. + /// This property will only be available if the server supports the + /// OBJECTID extension. + /// + /// The unique folder identifier. + public string? Id { + get; protected set; + } + /// /// Get a value indicating whether the folder is subscribed. /// /// /// Gets a value indicating whether the folder is subscribed. /// - /// true if the folder is subscribed; otherwise, false. + /// if the folder is subscribed; otherwise, . public bool IsSubscribed { get { return (Attributes & FolderAttributes.Subscribed) != 0; } } @@ -208,32 +334,31 @@ public bool IsSubscribed { /// /// Gets a value indicating whether the folder is currently open. /// - /// true if the folder is currently open; otherwise, false. + /// if the folder is currently open; otherwise, . public abstract bool IsOpen { get; } /// - /// Get a value indicating whether the folder exists. + /// Get whether or not the folder can be opened. /// /// - /// Gets a value indicating whether the folder exists. + /// Gets whether or not the folder can be opened. /// - /// true if the folder exists; otherwise, false. - public bool Exists { - get { return (Attributes & FolderAttributes.NonExistent) == 0; } + /// if the folder can be opened; otherwise, . + public bool CanOpen { + get { return (Attributes & (FolderAttributes.NoSelect | FolderAttributes.NonExistent)) == 0; } } /// - /// Get whether or not the folder supports mod-sequences. + /// Get a value indicating whether the folder exists. /// /// - /// If mod-sequences are not supported by the folder, then all of the APIs that take a modseq - /// argument will throw and should not be used. + /// Gets a value indicating whether the folder exists. /// - /// true if supports mod-sequences; otherwise, false. - public bool SupportsModSeq { - get; protected set; + /// if the folder exists; otherwise, . + public bool Exists { + get { return (Attributes & FolderAttributes.NonExistent) == 0; } } /// @@ -284,13 +409,28 @@ public uint? AppendLimit { get; protected set; } + /// + /// Get the size of the folder. + /// + /// + /// Gets the size of the folder in bytes. + /// If the value is not set, then the size is unspecified. + /// + /// The size of the folder, in bytes. + public ulong? Size { + get; protected set; + } + /// /// Get the index of the first unread message in the folder. /// /// - /// This value will only be set after the folder has been opened. + /// Gets the index of the first unread message in the folder. + /// This value will only be set after the folder has been opened. + /// A value of -1 indicates that there are no unread messages in the folder or that the server + /// has not provided the index of the first unread message. /// - /// The index of the first unread message. + /// The index of the first unread message or -1 if there are no unread messages in the folder. public int FirstUnread { get; protected set; } @@ -310,7 +450,7 @@ public int Unread { } /// - /// Get the number of recently added messages in the folder. + /// Get the number of recently delivered messages in the folder. /// /// /// Gets the number of recently delivered messages in the folder. @@ -318,7 +458,7 @@ public int Unread { /// /// with or by opening the folder. /// - /// The number of recently added messages. + /// The number of recently delivered messages. public int Recent { get; protected set; } @@ -332,11 +472,33 @@ public int Recent { /// /// with or by opening the folder. /// + /// + /// + /// /// The total number of messages. public int Count { get; protected set; } + /// + /// Get the threading algorithms supported by the folder. + /// + /// + /// Gets the threading algorithms supported by the folder. + /// + /// The supported threading algorithms. + public abstract HashSet ThreadingAlgorithms { get; } + + /// + /// Determine whether or not a supports a feature. + /// + /// + /// Determines whether or not a supports a feature. + /// + /// The desired feature. + /// if the feature is supported; otherwise, . + public abstract bool Supports (FolderFeature feature); + /// /// Opens the folder using the requested folder access. /// @@ -387,7 +549,7 @@ public int Count { /// /// The command failed. /// - public abstract FolderAccess Open (FolderAccess access, uint uidValidity, ulong highestModSeq, IList uids, CancellationToken cancellationToken = default (CancellationToken)); + public abstract FolderAccess Open (FolderAccess access, uint uidValidity, ulong highestModSeq, IList uids, CancellationToken cancellationToken = default); /// /// Asynchronously opens the folder using the requested folder access. @@ -439,23 +601,7 @@ public int Count { /// /// The command failed. /// - public virtual Task OpenAsync (FolderAccess access, uint uidValidity, ulong highestModSeq, IList uids, CancellationToken cancellationToken = default (CancellationToken)) - { - if (access != FolderAccess.ReadOnly && access != FolderAccess.ReadWrite) - throw new ArgumentOutOfRangeException (nameof (access)); - - if (uids == null) - throw new ArgumentNullException (nameof (uids)); - - if (uids.Count == 0) - throw new ArgumentException ("No uids were specified.", nameof (uids)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return Open (access, uidValidity, highestModSeq, uids, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task OpenAsync (FolderAccess access, uint uidValidity, ulong highestModSeq, IList uids, CancellationToken cancellationToken = default); /// /// Open the folder using the requested folder access. @@ -493,7 +639,7 @@ public int Count { /// /// The command failed. /// - public abstract FolderAccess Open (FolderAccess access, CancellationToken cancellationToken = default (CancellationToken)); + public abstract FolderAccess Open (FolderAccess access, CancellationToken cancellationToken = default); /// /// Asynchronously open the folder using the requested folder access. @@ -531,17 +677,7 @@ public int Count { /// /// The command failed. /// - public virtual Task OpenAsync (FolderAccess access, CancellationToken cancellationToken = default (CancellationToken)) - { - if (access != FolderAccess.ReadOnly && access != FolderAccess.ReadWrite) - throw new ArgumentOutOfRangeException (nameof (access)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return Open (access, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task OpenAsync (FolderAccess access, CancellationToken cancellationToken = default); /// /// Close the folder, optionally expunging the messages marked for deletion. @@ -549,7 +685,7 @@ public int Count { /// /// Closes the folder, optionally expunging the messages marked for deletion. /// - /// If set to true, expunge. + /// If set to , expunge. /// The cancellation token. /// /// The has been disposed. @@ -575,7 +711,7 @@ public int Count { /// /// The command failed. /// - public abstract void Close (bool expunge = false, CancellationToken cancellationToken = default (CancellationToken)); + public abstract void Close (bool expunge = false, CancellationToken cancellationToken = default); /// /// Asynchronously close the folder, optionally expunging the messages marked for deletion. @@ -584,7 +720,7 @@ public int Count { /// Asynchronously closes the folder, optionally expunging the messages marked for deletion. /// /// An asynchronous task context. - /// If set to true, expunge. + /// If set to , expunge. /// The cancellation token. /// /// The has been disposed. @@ -610,14 +746,7 @@ public int Count { /// /// The command failed. /// - public virtual Task CloseAsync (bool expunge = false, CancellationToken cancellationToken = default (CancellationToken)) - { - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - Close (expunge, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task CloseAsync (bool expunge = false, CancellationToken cancellationToken = default); /// /// Create a new subfolder with the given name. @@ -627,10 +756,10 @@ public int Count { /// /// The created folder. /// The name of the folder to create. - /// true if the folder will be used to contain messages; otherwise false. + /// if the folder will be used to contain messages; otherwise, . /// The cancellation token. /// - /// is null. + /// is . /// /// /// is empty. @@ -659,7 +788,7 @@ public int Count { /// /// The command failed. /// - public abstract IMailFolder Create (string name, bool isMessageFolder, CancellationToken cancellationToken = default (CancellationToken)); + public abstract IMailFolder? Create (string name, bool isMessageFolder, CancellationToken cancellationToken = default); /// /// Asynchronously create a new subfolder with the given name. @@ -669,10 +798,10 @@ public int Count { /// /// The created folder. /// The name of the folder to create. - /// true if the folder will be used to contain messages; otherwise false. + /// if the folder will be used to contain messages; otherwise, . /// The cancellation token. /// - /// is null. + /// is . /// /// /// is empty. @@ -701,20 +830,7 @@ public int Count { /// /// The command failed. /// - public virtual Task CreateAsync (string name, bool isMessageFolder, CancellationToken cancellationToken = default (CancellationToken)) - { - if (name == null) - throw new ArgumentNullException (nameof (name)); - - if (name.Length == 0 || name.IndexOf (DirectorySeparator) != -1) - throw new ArgumentException ("The name is not a legal folder name.", nameof (name)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return Create (name, isMessageFolder, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task CreateAsync (string name, bool isMessageFolder, CancellationToken cancellationToken = default); /// /// Create a new subfolder with the given name. @@ -727,9 +843,9 @@ public int Count { /// A list of special uses for the folder being created. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// is empty. @@ -761,7 +877,7 @@ public int Count { /// /// The command failed. /// - public abstract IMailFolder Create (string name, IEnumerable specialUses, CancellationToken cancellationToken = default (CancellationToken)); + public abstract IMailFolder? Create (string name, IEnumerable specialUses, CancellationToken cancellationToken = default); /// /// Asynchronously create a new subfolder with the given name. @@ -774,9 +890,9 @@ public int Count { /// A list of special uses for the folder being created. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// is empty. @@ -808,23 +924,7 @@ public int Count { /// /// The command failed. /// - public virtual Task CreateAsync (string name, IEnumerable specialUses, CancellationToken cancellationToken = default (CancellationToken)) - { - if (name == null) - throw new ArgumentNullException (nameof (name)); - - if (name.Length == 0 || name.IndexOf (DirectorySeparator) != -1) - throw new ArgumentException ("The name is not a legal folder name.", nameof (name)); - - if (specialUses == null) - throw new ArgumentNullException (nameof (specialUses)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return Create (name, specialUses, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task CreateAsync (string name, IEnumerable specialUses, CancellationToken cancellationToken = default); /// /// Create a new subfolder with the given name. @@ -837,7 +937,7 @@ public int Count { /// The special use for the folder being created. /// The cancellation token. /// - /// is null. + /// is . /// /// /// is empty. @@ -869,7 +969,7 @@ public int Count { /// /// The command failed. /// - public virtual IMailFolder Create (string name, SpecialFolder specialUse, CancellationToken cancellationToken = default (CancellationToken)) + public virtual IMailFolder? Create (string name, SpecialFolder specialUse, CancellationToken cancellationToken = default) { return Create (name, new [] { specialUse }, cancellationToken); } @@ -885,7 +985,7 @@ public int Count { /// The special use for the folder being created. /// The cancellation token. /// - /// is null. + /// is . /// /// /// is empty. @@ -917,19 +1017,9 @@ public int Count { /// /// The command failed. /// - public virtual Task CreateAsync (string name, SpecialFolder specialUse, CancellationToken cancellationToken = default (CancellationToken)) + public virtual Task CreateAsync (string name, SpecialFolder specialUse, CancellationToken cancellationToken = default) { - if (name == null) - throw new ArgumentNullException (nameof (name)); - - if (name.Length == 0 || name.IndexOf (DirectorySeparator) != -1) - throw new ArgumentException ("The name is not a legal folder name.", nameof (name)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return Create (name, specialUse, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); + return CreateAsync (name, new [] { specialUse }, cancellationToken); } /// @@ -942,9 +1032,9 @@ public int Count { /// The new name of the folder. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// does not belong to the . @@ -978,7 +1068,7 @@ public int Count { /// /// The command failed. /// - public abstract void Rename (IMailFolder parent, string name, CancellationToken cancellationToken = default (CancellationToken)); + public abstract void Rename (IMailFolder parent, string name, CancellationToken cancellationToken = default); /// /// Asynchronously rename the folder. @@ -991,9 +1081,9 @@ public int Count { /// The new name of the folder. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// does not belong to the . @@ -1027,26 +1117,7 @@ public int Count { /// /// The command failed. /// - public virtual Task RenameAsync (IMailFolder parent, string name, CancellationToken cancellationToken = default (CancellationToken)) - { - if (parent == null) - throw new ArgumentNullException (nameof (parent)); - - if (name == null) - throw new ArgumentNullException (nameof (name)); - - if (name.Length == 0 || name.IndexOf (parent.DirectorySeparator) != -1) - throw new ArgumentException ("The name is not a legal folder name.", nameof (name)); - - if (IsNamespace) - throw new InvalidOperationException ("Cannot rename this folder."); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - Rename (parent, name, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task RenameAsync (IMailFolder parent, string name, CancellationToken cancellationToken = default); /// /// Delete the folder. @@ -1082,7 +1153,7 @@ public int Count { /// /// The command failed. /// - public abstract void Delete (CancellationToken cancellationToken = default (CancellationToken)); + public abstract void Delete (CancellationToken cancellationToken = default); /// /// Asynchronously delete the folder. @@ -1119,17 +1190,7 @@ public int Count { /// /// The command failed. /// - public virtual Task DeleteAsync (CancellationToken cancellationToken = default (CancellationToken)) - { - if (IsNamespace) - throw new InvalidOperationException ("Cannot delete this folder."); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - Delete (cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task DeleteAsync (CancellationToken cancellationToken = default); /// /// Subscribe to the folder. @@ -1159,7 +1220,7 @@ public int Count { /// /// The command failed. /// - public abstract void Subscribe (CancellationToken cancellationToken = default (CancellationToken)); + public abstract void Subscribe (CancellationToken cancellationToken = default); /// /// Asynchronously subscribe to the folder. @@ -1190,14 +1251,7 @@ public int Count { /// /// The command failed. /// - public virtual Task SubscribeAsync (CancellationToken cancellationToken = default (CancellationToken)) - { - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - Subscribe (cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task SubscribeAsync (CancellationToken cancellationToken = default); /// /// Unsubscribe from the folder. @@ -1227,7 +1281,7 @@ public int Count { /// /// The command failed. /// - public abstract void Unsubscribe (CancellationToken cancellationToken = default (CancellationToken)); + public abstract void Unsubscribe (CancellationToken cancellationToken = default); /// /// Asynchronously unsubscribe from the folder. @@ -1258,14 +1312,7 @@ public int Count { /// /// The command failed. /// - public virtual Task UnsubscribeAsync (CancellationToken cancellationToken = default (CancellationToken)) - { - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - Unsubscribe (cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task UnsubscribeAsync (CancellationToken cancellationToken = default); /// /// Get the subfolders. @@ -1278,7 +1325,7 @@ public int Count { /// /// The subfolders. /// The status items to pre-populate. - /// If set to true, only subscribed folders will be listed. + /// If set to , only subscribed folders will be listed. /// The cancellation token. /// /// The has been disposed. @@ -1301,7 +1348,7 @@ public int Count { /// /// The command failed. /// - public abstract IEnumerable GetSubfolders (StatusItems items, bool subscribedOnly = false, CancellationToken cancellationToken = default (CancellationToken)); + public abstract IList GetSubfolders (StatusItems items, bool subscribedOnly = false, CancellationToken cancellationToken = default); /// /// Asynchronously get the subfolders. @@ -1314,7 +1361,7 @@ public int Count { /// /// The subfolders. /// The status items to pre-populate. - /// If set to true, only subscribed folders will be listed. + /// If set to , only subscribed folders will be listed. /// The cancellation token. /// /// The has been disposed. @@ -1337,14 +1384,7 @@ public int Count { /// /// The command failed. /// - public virtual Task> GetSubfoldersAsync (StatusItems items, bool subscribedOnly = false, CancellationToken cancellationToken = default (CancellationToken)) - { - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return GetSubfolders (items, subscribedOnly, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task> GetSubfoldersAsync (StatusItems items, bool subscribedOnly = false, CancellationToken cancellationToken = default); /// /// Get the subfolders. @@ -1353,7 +1393,7 @@ public int Count { /// Gets the subfolders. /// /// The subfolders. - /// If set to true, only subscribed folders will be listed. + /// If set to , only subscribed folders will be listed. /// The cancellation token. /// /// The has been disposed. @@ -1376,7 +1416,7 @@ public int Count { /// /// The command failed. /// - public virtual IEnumerable GetSubfolders (bool subscribedOnly = false, CancellationToken cancellationToken = default (CancellationToken)) + public virtual IList GetSubfolders (bool subscribedOnly = false, CancellationToken cancellationToken = default) { return GetSubfolders (StatusItems.None, subscribedOnly, cancellationToken); } @@ -1388,7 +1428,7 @@ public int Count { /// Asynchronously gets the subfolders. /// /// The subfolders. - /// If set to true, only subscribed folders will be listed. + /// If set to , only subscribed folders will be listed. /// The cancellation token. /// /// The has been disposed. @@ -1411,13 +1451,9 @@ public int Count { /// /// The command failed. /// - public virtual Task> GetSubfoldersAsync (bool subscribedOnly = false, CancellationToken cancellationToken = default (CancellationToken)) + public virtual Task> GetSubfoldersAsync (bool subscribedOnly = false, CancellationToken cancellationToken = default) { - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return GetSubfolders (subscribedOnly, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); + return GetSubfoldersAsync (StatusItems.None, subscribedOnly, cancellationToken); } /// @@ -1430,7 +1466,7 @@ public int Count { /// The name of the subfolder. /// The cancellation token. /// - /// is null. + /// is . /// /// /// is either an empty string or contains the . @@ -1459,7 +1495,7 @@ public int Count { /// /// The command failed. /// - public abstract IMailFolder GetSubfolder (string name, CancellationToken cancellationToken = default (CancellationToken)); + public abstract IMailFolder GetSubfolder (string name, CancellationToken cancellationToken = default); /// /// Asynchronously get the specified subfolder. @@ -1471,7 +1507,7 @@ public int Count { /// The name of the subfolder. /// The cancellation token. /// - /// is null. + /// is . /// /// /// is either an empty string or contains the . @@ -1500,20 +1536,7 @@ public int Count { /// /// The command failed. /// - public virtual Task GetSubfolderAsync (string name, CancellationToken cancellationToken = default (CancellationToken)) - { - if (name == null) - throw new ArgumentNullException (nameof (name)); - - if (name.Length == 0 || name.IndexOf (DirectorySeparator) != -1) - throw new ArgumentException ("The name of the subfolder is invalid.", nameof (name)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return GetSubfolder (name, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task GetSubfolderAsync (string name, CancellationToken cancellationToken = default); /// /// Force the server to flush its state for the folder. @@ -1546,7 +1569,7 @@ public int Count { /// /// The command failed. /// - public abstract void Check (CancellationToken cancellationToken = default (CancellationToken)); + public abstract void Check (CancellationToken cancellationToken = default); /// /// Asynchronously force the server to flush its state for the folder. @@ -1580,14 +1603,7 @@ public int Count { /// /// The command failed. /// - public virtual Task CheckAsync (CancellationToken cancellationToken = default (CancellationToken)) - { - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - Check (cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task CheckAsync (CancellationToken cancellationToken = default); /// /// Update the values of the specified items. @@ -1631,7 +1647,7 @@ public int Count { /// /// The command failed. /// - public abstract void Status (StatusItems items, CancellationToken cancellationToken = default (CancellationToken)); + public abstract void Status (StatusItems items, CancellationToken cancellationToken = default); /// /// Asynchronously update the values of the specified items. @@ -1673,14 +1689,7 @@ public int Count { /// /// The command failed. /// - public virtual Task StatusAsync (StatusItems items, CancellationToken cancellationToken = default (CancellationToken)) - { - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - Status (items, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task StatusAsync (StatusItems items, CancellationToken cancellationToken = default); /// /// Get the complete access control list for the folder. @@ -1714,7 +1723,7 @@ public int Count { /// /// The command failed. /// - public abstract AccessControlList GetAccessControlList (CancellationToken cancellationToken = default (CancellationToken)); + public abstract AccessControlList GetAccessControlList (CancellationToken cancellationToken = default); /// /// Asynchronously get the complete access control list for the folder. @@ -1748,14 +1757,7 @@ public int Count { /// /// The command failed. /// - public virtual Task GetAccessControlListAsync (CancellationToken cancellationToken = default (CancellationToken)) - { - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return GetAccessControlList (cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task GetAccessControlListAsync (CancellationToken cancellationToken = default); /// /// Get the access rights for a particular identifier. @@ -1767,7 +1769,7 @@ public int Count { /// The identifier name. /// The cancellation token. /// - /// is null. + /// is . /// /// /// The has been disposed. @@ -1793,7 +1795,7 @@ public int Count { /// /// The command failed. /// - public abstract AccessRights GetAccessRights (string name, CancellationToken cancellationToken = default (CancellationToken)); + public abstract AccessRights GetAccessRights (string name, CancellationToken cancellationToken = default); /// /// Asynchronously get the access rights for a particular identifier. @@ -1805,7 +1807,7 @@ public int Count { /// The identifier name. /// The cancellation token. /// - /// is null. + /// is . /// /// /// The has been disposed. @@ -1831,17 +1833,7 @@ public int Count { /// /// The command failed. /// - public virtual Task GetAccessRightsAsync (string name, CancellationToken cancellationToken = default (CancellationToken)) - { - if (name == null) - throw new ArgumentNullException (nameof (name)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return GetAccessRights (name, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task GetAccessRightsAsync (string name, CancellationToken cancellationToken = default); /// /// Get the access rights for the current authenticated user. @@ -1875,7 +1867,7 @@ public int Count { /// /// The command failed. /// - public abstract AccessRights GetMyAccessRights (CancellationToken cancellationToken = default (CancellationToken)); + public abstract AccessRights GetMyAccessRights (CancellationToken cancellationToken = default); /// /// Asynchronously get the access rights for the current authenticated user. @@ -1909,14 +1901,7 @@ public int Count { /// /// The command failed. /// - public virtual Task GetMyAccessRightsAsync (CancellationToken cancellationToken = default (CancellationToken)) - { - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return GetMyAccessRights (cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task GetMyAccessRightsAsync (CancellationToken cancellationToken = default); /// /// Add access rights for the specified identity. @@ -1928,9 +1913,9 @@ public int Count { /// The access rights. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// The has been disposed. @@ -1956,7 +1941,7 @@ public int Count { /// /// The command failed. /// - public abstract void AddAccessRights (string name, AccessRights rights, CancellationToken cancellationToken = default (CancellationToken)); + public abstract void AddAccessRights (string name, AccessRights rights, CancellationToken cancellationToken = default); /// /// Asynchronously add access rights for the specified identity. @@ -1969,9 +1954,9 @@ public int Count { /// The access rights. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// The has been disposed. @@ -1997,20 +1982,7 @@ public int Count { /// /// The command failed. /// - public virtual Task AddAccessRightsAsync (string name, AccessRights rights, CancellationToken cancellationToken = default (CancellationToken)) - { - if (name == null) - throw new ArgumentNullException (nameof (name)); - - if (rights == null) - throw new ArgumentNullException (nameof (rights)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - AddAccessRights (name, rights, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task AddAccessRightsAsync (string name, AccessRights rights, CancellationToken cancellationToken = default); /// /// Remove access rights for the specified identity. @@ -2022,9 +1994,9 @@ public int Count { /// The access rights. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// The has been disposed. @@ -2050,7 +2022,7 @@ public int Count { /// /// The command failed. /// - public abstract void RemoveAccessRights (string name, AccessRights rights, CancellationToken cancellationToken = default (CancellationToken)); + public abstract void RemoveAccessRights (string name, AccessRights rights, CancellationToken cancellationToken = default); /// /// Asynchronously remove access rights for the specified identity. @@ -2063,9 +2035,9 @@ public int Count { /// The access rights. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// The has been disposed. @@ -2091,20 +2063,7 @@ public int Count { /// /// The command failed. /// - public virtual Task RemoveAccessRightsAsync (string name, AccessRights rights, CancellationToken cancellationToken = default (CancellationToken)) - { - if (name == null) - throw new ArgumentNullException (nameof (name)); - - if (rights == null) - throw new ArgumentNullException (nameof (rights)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - RemoveAccessRights (name, rights, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task RemoveAccessRightsAsync (string name, AccessRights rights, CancellationToken cancellationToken = default); /// /// Set the access rights for the specified identity. @@ -2116,9 +2075,9 @@ public int Count { /// The access rights. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// The has been disposed. @@ -2144,7 +2103,7 @@ public int Count { /// /// The command failed. /// - public abstract void SetAccessRights (string name, AccessRights rights, CancellationToken cancellationToken = default (CancellationToken)); + public abstract void SetAccessRights (string name, AccessRights rights, CancellationToken cancellationToken = default); /// /// Asynchronously set the access rights for the specified identity. @@ -2157,9 +2116,9 @@ public int Count { /// The access rights. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// The has been disposed. @@ -2185,20 +2144,7 @@ public int Count { /// /// The command failed. /// - public virtual Task SetAccessRightsAsync (string name, AccessRights rights, CancellationToken cancellationToken = default (CancellationToken)) - { - if (name == null) - throw new ArgumentNullException (nameof (name)); - - if (rights == null) - throw new ArgumentNullException (nameof (rights)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - SetAccessRights (name, rights, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task SetAccessRightsAsync (string name, AccessRights rights, CancellationToken cancellationToken = default); /// /// Remove all access rights for the given identity. @@ -2209,7 +2155,7 @@ public int Count { /// The identity name. /// The cancellation token. /// - /// is null. + /// is . /// /// /// The has been disposed. @@ -2235,7 +2181,7 @@ public int Count { /// /// The command failed. /// - public abstract void RemoveAccess (string name, CancellationToken cancellationToken = default (CancellationToken)); + public abstract void RemoveAccess (string name, CancellationToken cancellationToken = default); /// /// Asynchronously remove all access rights for the given identity. @@ -2247,7 +2193,7 @@ public int Count { /// The identity name. /// The cancellation token. /// - /// is null. + /// is . /// /// /// The has been disposed. @@ -2273,17 +2219,7 @@ public int Count { /// /// The command failed. /// - public virtual Task RemoveAccessAsync (string name, CancellationToken cancellationToken = default (CancellationToken)) - { - if (name == null) - throw new ArgumentNullException (nameof (name)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - RemoveAccess (name, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task RemoveAccessAsync (string name, CancellationToken cancellationToken = default); /// /// Get the quota information for the folder. @@ -2319,7 +2255,7 @@ public int Count { /// /// The command failed. /// - public abstract FolderQuota GetQuota (CancellationToken cancellationToken = default (CancellationToken)); + public abstract FolderQuota GetQuota (CancellationToken cancellationToken = default); /// /// Asynchronously get the quota information for the folder. @@ -2355,14 +2291,7 @@ public int Count { /// /// The command failed. /// - public virtual Task GetQuotaAsync (CancellationToken cancellationToken = default (CancellationToken)) - { - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return GetQuota (cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task GetQuotaAsync (CancellationToken cancellationToken = default); /// /// Set the quota limits for the folder. @@ -2373,8 +2302,8 @@ public int Count { /// property. /// /// The updated folder quota. - /// If not null, sets the maximum number of messages to allow. - /// If not null, sets the maximum storage size (in kilobytes). + /// If not , sets the maximum number of messages to allow. + /// If not , sets the maximum storage size (in kilobytes). /// The cancellation token. /// /// The has been disposed. @@ -2400,7 +2329,7 @@ public int Count { /// /// The command failed. /// - public abstract FolderQuota SetQuota (uint? messageLimit, uint? storageLimit, CancellationToken cancellationToken = default (CancellationToken)); + public abstract FolderQuota SetQuota (uint? messageLimit, uint? storageLimit, CancellationToken cancellationToken = default); /// /// Asynchronously set the quota limits for the folder. @@ -2411,8 +2340,8 @@ public int Count { /// property. /// /// The updated folder quota. - /// If not null, sets the maximum number of messages to allow. - /// If not null, sets the maximum storage size (in kilobytes). + /// If not , sets the maximum number of messages to allow. + /// If not , sets the maximum storage size (in kilobytes). /// The cancellation token. /// /// The has been disposed. @@ -2438,17 +2367,10 @@ public int Count { /// /// The command failed. /// - public virtual Task SetQuotaAsync (uint? messageLimit, uint? storageLimit, CancellationToken cancellationToken = default (CancellationToken)) - { - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return SetQuota (messageLimit, storageLimit, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task SetQuotaAsync (uint? messageLimit, uint? storageLimit, CancellationToken cancellationToken = default); /// - /// Gets the specified metadata. + /// Get the specified metadata. /// /// /// Gets the specified metadata. @@ -2480,7 +2402,7 @@ public int Count { /// /// The command failed. /// - public abstract string GetMetadata (MetadataTag tag, CancellationToken cancellationToken = default (CancellationToken)); + public abstract string? GetMetadata (MetadataTag tag, CancellationToken cancellationToken = default); /// /// Asynchronously gets the specified metadata. @@ -2515,17 +2437,10 @@ public int Count { /// /// The command failed. /// - public virtual Task GetMetadataAsync (MetadataTag tag, CancellationToken cancellationToken = default (CancellationToken)) - { - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return GetMetadata (tag, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task GetMetadataAsync (MetadataTag tag, CancellationToken cancellationToken = default); /// - /// Gets the specified metadata. + /// Get the specified metadata. /// /// /// Gets the specified metadata. @@ -2534,7 +2449,7 @@ public int Count { /// The metadata tags. /// The cancellation token. /// - /// is null. + /// is . /// /// /// The has been disposed. @@ -2560,7 +2475,7 @@ public int Count { /// /// The command failed. /// - public MetadataCollection GetMetadata (IEnumerable tags, CancellationToken cancellationToken = default (CancellationToken)) + public MetadataCollection GetMetadata (IEnumerable tags, CancellationToken cancellationToken = default) { return GetMetadata (new MetadataOptions (), tags, cancellationToken); } @@ -2575,7 +2490,7 @@ public int Count { /// The metadata tags. /// The cancellation token. /// - /// is null. + /// is . /// /// /// The has been disposed. @@ -2601,17 +2516,13 @@ public int Count { /// /// The command failed. /// - public virtual Task GetMetadataAsync (IEnumerable tags, CancellationToken cancellationToken = default (CancellationToken)) + public virtual Task GetMetadataAsync (IEnumerable tags, CancellationToken cancellationToken = default) { - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return GetMetadata (tags, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); + return GetMetadataAsync (new MetadataOptions (), tags, cancellationToken); } /// - /// Gets the specified metadata. + /// Get the specified metadata. /// /// /// Gets the specified metadata. @@ -2621,9 +2532,9 @@ public int Count { /// The metadata tags. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// The has been disposed. @@ -2649,7 +2560,7 @@ public int Count { /// /// The command failed. /// - public abstract MetadataCollection GetMetadata (MetadataOptions options, IEnumerable tags, CancellationToken cancellationToken = default (CancellationToken)); + public abstract MetadataCollection GetMetadata (MetadataOptions options, IEnumerable tags, CancellationToken cancellationToken = default); /// /// Asynchronously gets the specified metadata. @@ -2662,9 +2573,9 @@ public int Count { /// The metadata tags. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// The has been disposed. @@ -2690,14 +2601,7 @@ public int Count { /// /// The command failed. /// - public virtual Task GetMetadataAsync (MetadataOptions options, IEnumerable tags, CancellationToken cancellationToken = default (CancellationToken)) - { - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return GetMetadata (options, tags, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task GetMetadataAsync (MetadataOptions options, IEnumerable tags, CancellationToken cancellationToken = default); /// /// Sets the specified metadata. @@ -2708,7 +2612,7 @@ public int Count { /// The metadata. /// The cancellation token. /// - /// is null. + /// is . /// /// /// The has been disposed. @@ -2734,7 +2638,7 @@ public int Count { /// /// The command failed. /// - public abstract void SetMetadata (MetadataCollection metadata, CancellationToken cancellationToken = default (CancellationToken)); + public abstract void SetMetadata (MetadataCollection metadata, CancellationToken cancellationToken = default); /// /// Asynchronously sets the specified metadata. @@ -2746,7 +2650,7 @@ public int Count { /// The metadata. /// The cancellation token. /// - /// is null. + /// is . /// /// /// The has been disposed. @@ -2772,14 +2676,7 @@ public int Count { /// /// The command failed. /// - public virtual Task SetMetadataAsync (MetadataCollection metadata, CancellationToken cancellationToken = default (CancellationToken)) - { - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - SetMetadata (metadata, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task SetMetadataAsync (MetadataCollection metadata, CancellationToken cancellationToken = default); /// /// Expunge the folder, permanently removing all messages marked for deletion. @@ -2818,7 +2715,7 @@ public int Count { /// /// The command failed. /// - public abstract void Expunge (CancellationToken cancellationToken = default (CancellationToken)); + public abstract void Expunge (CancellationToken cancellationToken = default); /// /// Asynchronously expunge the folder, permanently removing all messages marked for deletion. @@ -2858,14 +2755,7 @@ public int Count { /// /// The command failed. /// - public virtual Task ExpungeAsync (CancellationToken cancellationToken = default (CancellationToken)) - { - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - Expunge (cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task ExpungeAsync (CancellationToken cancellationToken = default); /// /// Expunge the specified uids, permanently removing them from the folder. @@ -2882,7 +2772,7 @@ public int Count { /// The message uids. /// The cancellation token. /// - /// is null. + /// is . /// /// /// One or more of the is invalid. @@ -2911,7 +2801,7 @@ public int Count { /// /// The command failed. /// - public abstract void Expunge (IList uids, CancellationToken cancellationToken = default (CancellationToken)); + public abstract void Expunge (IList uids, CancellationToken cancellationToken = default); /// /// Asynchronously expunge the specified uids, permanently removing them from the folder. @@ -2929,7 +2819,7 @@ public int Count { /// The message uids. /// The cancellation token. /// - /// is null. + /// is . /// /// /// One or more of the is invalid. @@ -2958,31 +2848,19 @@ public int Count { /// /// The command failed. /// - public virtual Task ExpungeAsync (IList uids, CancellationToken cancellationToken = default (CancellationToken)) - { - if (uids == null) - throw new ArgumentNullException (nameof (uids)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - Expunge (uids, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task ExpungeAsync (IList uids, CancellationToken cancellationToken = default); /// - /// Append the specified message to the folder. + /// Append a message to the folder. /// /// - /// Appends the specified message to the folder and returns the UniqueId assigned to the message. + /// Appends a message to the folder and returns the UniqueId assigned to the message. /// - /// The UID of the appended message, if available; otherwise, null. - /// The message. - /// The message flags. + /// The UID of the appended message, if available; otherwise, . + /// The append request. /// The cancellation token. - /// The progress reporting mechanism. /// - /// is null. + /// is . /// /// /// The has been disposed. @@ -2993,12 +2871,20 @@ public int Count { /// /// The is not authenticated. /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// /// /// The does not exist. /// /// /// The operation was canceled via the cancellation token. /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// -or- + /// The request included annotations but the folder does not support annotations. + /// /// /// An I/O error occurred. /// @@ -3008,24 +2894,22 @@ public int Count { /// /// The command failed. /// - public virtual UniqueId? Append (MimeMessage message, MessageFlags flags = MessageFlags.None, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + public virtual UniqueId? Append (IAppendRequest request, CancellationToken cancellationToken = default) { - return Append (FormatOptions.Default, message, flags, cancellationToken, progress); + return Append (FormatOptions.Default, request, cancellationToken); } /// - /// Asynchronously append the specified message to the folder. + /// Asynchronously append a message to the folder. /// /// - /// Asynchronously appends the specified message to the folder and returns the UniqueId assigned to the message. + /// Asynchronously appends a message to the folder and returns the UniqueId assigned to the message. /// - /// The UID of the appended message, if available; otherwise, null. - /// The message. - /// The message flags. + /// The UID of the appended message, if available; otherwise, . + /// The append request. /// The cancellation token. - /// The progress reporting mechanism. /// - /// is null. + /// is . /// /// /// The has been disposed. @@ -3036,12 +2920,20 @@ public int Count { /// /// The is not authenticated. /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// /// /// The does not exist. /// /// /// The operation was canceled via the cancellation token. /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// -or- + /// The request included annotations but the folder does not support annotations. + /// /// /// An I/O error occurred. /// @@ -3051,32 +2943,25 @@ public int Count { /// /// The command failed. /// - public virtual Task AppendAsync (MimeMessage message, MessageFlags flags = MessageFlags.None, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + public virtual Task AppendAsync (IAppendRequest request, CancellationToken cancellationToken = default) { - if (message == null) - throw new ArgumentNullException (nameof (message)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return Append (message, flags, cancellationToken, progress); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); + return AppendAsync (FormatOptions.Default, request, cancellationToken); } /// - /// Append the specified message to the folder. + /// Append a message to the folder. /// /// - /// Appends the specified message to the folder and returns the UniqueId assigned to the message. + /// Appends a message to the folder and returns the UniqueId assigned to the message. /// - /// The UID of the appended message, if available; otherwise, null. - /// The message. - /// The message flags. - /// The received date of the message. + /// The UID of the appended message, if available; otherwise, . + /// The formatting options. + /// The append request. /// The cancellation token. - /// The progress reporting mechanism. /// - /// is null. + /// is . + /// -or- + /// is . /// /// /// The has been disposed. @@ -3087,12 +2972,20 @@ public int Count { /// /// The is not authenticated. /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// /// /// The does not exist. /// /// /// The operation was canceled via the cancellation token. /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// -or- + /// The request included annotations but the folder does not support annotations. + /// /// /// An I/O error occurred. /// @@ -3102,25 +2995,22 @@ public int Count { /// /// The command failed. /// - public virtual UniqueId? Append (MimeMessage message, MessageFlags flags, DateTimeOffset date, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) - { - return Append (FormatOptions.Default, message, flags, date, cancellationToken, progress); - } + public abstract UniqueId? Append (FormatOptions options, IAppendRequest request, CancellationToken cancellationToken = default); /// - /// Asynchronously append the specified message to the folder. + /// Asynchronously append a message to the folder. /// /// - /// Asynchronously appends the specified message to the folder and returns the UniqueId assigned to the message. + /// Asynchronously appends a message to the folder and returns the UniqueId assigned to the message. /// - /// The UID of the appended message, if available; otherwise, null. - /// The message. - /// The message flags. - /// The received date of the message. + /// The UID of the appended message, if available; otherwise, . + /// The formatting options. + /// The append request. /// The cancellation token. - /// The progress reporting mechanism. /// - /// is null. + /// is . + /// -or- + /// is . /// /// /// The has been disposed. @@ -3131,12 +3021,20 @@ public int Count { /// /// The is not authenticated. /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// /// /// The does not exist. /// /// /// The operation was canceled via the cancellation token. /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// -or- + /// The request included annotations but the folder does not support annotations. + /// /// /// An I/O error occurred. /// @@ -3146,34 +3044,22 @@ public int Count { /// /// The command failed. /// - public virtual Task AppendAsync (MimeMessage message, MessageFlags flags, DateTimeOffset date, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) - { - if (message == null) - throw new ArgumentNullException (nameof (message)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return Append (message, flags, date, cancellationToken, progress); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task AppendAsync (FormatOptions options, IAppendRequest request, CancellationToken cancellationToken = default); /// - /// Append the specified message to the folder. + /// Append multiple messages to the folder. /// /// - /// Appends the specified message to the folder and returns the UniqueId assigned to the message. + /// Appends multiple messages to the folder and returns the UniqueIds assigned to the messages. /// - /// The UID of the appended message, if available; otherwise, null. - /// The formatting options. - /// The message. - /// The message flags. + /// The UIDs of the appended messages, if available; otherwise an empty array. + /// The append requests. /// The cancellation token. - /// The progress reporting mechanism. /// - /// is null. - /// -or- - /// is null. + /// is . + /// + /// + /// One or more of the is . /// /// /// The has been disposed. @@ -3184,17 +3070,19 @@ public int Count { /// /// The is not authenticated. /// - /// - /// The does not exist. - /// /// /// Internationalized formatting was requested but has not been enabled. /// + /// + /// The does not exist. + /// /// /// The operation was canceled via the cancellation token. /// /// - /// Internationalized formatting was requested but is not supported by the server. + /// Internationalized formatting was requested but is not supported by the server. + /// -or- + /// One ore more requests included annotations but the folder does not support annotations. /// /// /// An I/O error occurred. @@ -3205,24 +3093,25 @@ public int Count { /// /// The command failed. /// - public abstract UniqueId? Append (FormatOptions options, MimeMessage message, MessageFlags flags = MessageFlags.None, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + public virtual IList Append (IList requests, CancellationToken cancellationToken = default) + { + return Append (FormatOptions.Default, requests, cancellationToken); + } /// - /// Asynchronously append the specified message to the folder. + /// Asynchronously append multiple messages to the folder. /// /// - /// Asynchronously appends the specified message to the folder and returns the UniqueId assigned to the message. + /// Asynchronously appends multiple messages to the folder and returns the UniqueIds assigned to the messages. /// - /// The UID of the appended message, if available; otherwise, null. - /// The formatting options. - /// The message. - /// The message flags. + /// The UIDs of the appended messages, if available; otherwise an empty array. + /// The append requests. /// The cancellation token. - /// The progress reporting mechanism. /// - /// is null. - /// -or- - /// is null. + /// is . + /// + /// + /// One or more of the is . /// /// /// The has been disposed. @@ -3233,17 +3122,19 @@ public int Count { /// /// The is not authenticated. /// - /// - /// The does not exist. - /// /// /// Internationalized formatting was requested but has not been enabled. /// + /// + /// The does not exist. + /// /// /// The operation was canceled via the cancellation token. /// /// - /// Internationalized formatting was requested but is not supported by the server. + /// Internationalized formatting was requested but is not supported by the server. + /// -or- + /// One ore more requests included annotations but the folder does not support annotations. /// /// /// An I/O error occurred. @@ -3254,38 +3145,28 @@ public int Count { /// /// The command failed. /// - public virtual Task AppendAsync (FormatOptions options, MimeMessage message, MessageFlags flags = MessageFlags.None, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + public virtual Task> AppendAsync (IList requests, CancellationToken cancellationToken = default) { - if (options == null) - throw new ArgumentNullException (nameof (options)); - - if (message == null) - throw new ArgumentNullException (nameof (message)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return Append (options, message, flags, cancellationToken, progress); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); + return AppendAsync (FormatOptions.Default, requests, cancellationToken); } /// - /// Append the specified message to the folder. + /// Append multiple messages to the folder. /// /// - /// Appends the specified message to the folder and returns the UniqueId assigned to the message. + /// Appends multiple messages to the folder and returns the UniqueIds assigned to the messages. /// - /// The UID of the appended message, if available; otherwise, null. + /// The UIDs of the appended messages, if available; otherwise an empty array. /// The formatting options. - /// The message. - /// The message flags. - /// The received date of the message. + /// The append requests. /// The cancellation token. - /// The progress reporting mechanism. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . + /// + /// + /// One or more of the is . /// /// /// The has been disposed. @@ -3296,17 +3177,19 @@ public int Count { /// /// The is not authenticated. /// - /// - /// The does not exist. - /// /// /// Internationalized formatting was requested but has not been enabled. /// + /// + /// The does not exist. + /// /// /// The operation was canceled via the cancellation token. /// /// - /// Internationalized formatting was requested but is not supported by the server. + /// Internationalized formatting was requested but is not supported by the server. + /// -or- + /// One ore more requests included annotations but the folder does not support annotations. /// /// /// An I/O error occurred. @@ -3317,25 +3200,25 @@ public int Count { /// /// The command failed. /// - public abstract UniqueId? Append (FormatOptions options, MimeMessage message, MessageFlags flags, DateTimeOffset date, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + public abstract IList Append (FormatOptions options, IList requests, CancellationToken cancellationToken = default); /// - /// Asynchronously append the specified message to the folder. + /// Asynchronously append multiple messages to the folder. /// /// - /// Asynchronously appends the specified message to the folder and returns the UniqueId assigned to the message. + /// Asynchronously appends multiple messages to the folder and returns the UniqueIds assigned to the messages. /// - /// The UID of the appended message, if available; otherwise, null. + /// The UIDs of the appended messages, if available; otherwise an empty array. /// The formatting options. - /// The message. - /// The message flags. - /// The received date of the message. + /// The append requests. /// The cancellation token. - /// The progress reporting mechanism. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . + /// + /// + /// One or more of the is . /// /// /// The has been disposed. @@ -3346,17 +3229,19 @@ public int Count { /// /// The is not authenticated. /// - /// - /// The does not exist. - /// /// /// Internationalized formatting was requested but has not been enabled. /// + /// + /// The does not exist. + /// /// /// The operation was canceled via the cancellation token. /// /// - /// Internationalized formatting was requested but is not supported by the server. + /// Internationalized formatting was requested but is not supported by the server. + /// -or- + /// One ore more requests included annotations but the folder does not support annotations. /// /// /// An I/O error occurred. @@ -3367,41 +3252,25 @@ public int Count { /// /// The command failed. /// - public virtual Task AppendAsync (FormatOptions options, MimeMessage message, MessageFlags flags, DateTimeOffset date, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) - { - if (options == null) - throw new ArgumentNullException (nameof (options)); - - if (message == null) - throw new ArgumentNullException (nameof (message)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return Append (options, message, flags, date, cancellationToken, progress); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task> AppendAsync (FormatOptions options, IList requests, CancellationToken cancellationToken = default); /// - /// Append the specified messages to the folder. + /// Replace a message in the folder. /// /// - /// Appends the specified messages to the folder and returns the UniqueIds assigned to the messages. + /// Replaces a message in the folder and returns the UniqueId assigned to the new message. /// - /// The UIDs of the appended messages, if available; otherwise an empty array. - /// The array of messages to append to the folder. - /// The message flags to use for each message. + /// The UID of the new message, if available; otherwise, . + /// The UID of the message to be replaced. + /// The replace request. /// The cancellation token. - /// The progress reporting mechanism. /// - /// is null. - /// -or- - /// is null. + /// is . /// /// - /// One or more of the is null. + /// is invalid. /// -or- - /// The number of messages does not match the number of flags. + /// The destination folder does not belong to this . /// /// /// The has been disposed. @@ -3412,9 +3281,15 @@ public int Count { /// /// The is not authenticated. /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// /// /// The does not exist. /// + /// + /// The is not currently open in read-write mode. + /// /// /// The operation was canceled via the cancellation token. /// @@ -3425,33 +3300,30 @@ public int Count { /// The server's response contained unexpected tokens. /// /// - /// The command failed. + /// The server replied with a NO or BAD response. /// - public virtual IList Append (IList messages, IList flags, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + public virtual UniqueId? Replace (UniqueId uid, IReplaceRequest request, CancellationToken cancellationToken = default) { - return Append (FormatOptions.Default, messages, flags, cancellationToken, progress); + return Replace (FormatOptions.Default, uid, request, cancellationToken); } /// - /// Asynchronously append the specified messages to the folder. + /// Asynchronously replace a message in the folder. /// /// - /// Asynchronously appends the specified messages to the folder and returns the UniqueIds assigned to the messages. + /// Asynchronously replaces a message in the folder and returns the UniqueId assigned to the new message. /// - /// The UIDs of the appended messages, if available; otherwise an empty array. - /// The array of messages to append to the folder. - /// The message flags to use for each message. + /// The UID of the appended message, if available; otherwise, . + /// The UID of the message to be replaced. + /// The replace request. /// The cancellation token. - /// The progress reporting mechanism. /// - /// is null. - /// -or- - /// is null. + /// is . /// /// - /// One or more of the is null. + /// is invalid. /// -or- - /// The number of messages does not match the number of flags. + /// The destination folder does not belong to this . /// /// /// The has been disposed. @@ -3462,9 +3334,15 @@ public int Count { /// /// The is not authenticated. /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// /// /// The does not exist. /// + /// + /// The is not currently open in read-write mode. + /// /// /// The operation was canceled via the cancellation token. /// @@ -3475,54 +3353,33 @@ public int Count { /// The server's response contained unexpected tokens. /// /// - /// The command failed. + /// The server replied with a NO or BAD response. /// - public virtual Task> AppendAsync (IList messages, IList flags, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + public virtual Task ReplaceAsync (UniqueId uid, IReplaceRequest request, CancellationToken cancellationToken = default) { - if (messages == null) - throw new ArgumentNullException (nameof (messages)); - - for (int i = 0; i < messages.Count; i++) { - if (messages[i] == null) - throw new ArgumentException ("One or more of the messages is null."); - } - - if (flags == null) - throw new ArgumentNullException (nameof (flags)); - - if (messages.Count != flags.Count) - throw new ArgumentException ("The number of messages and the number of flags must be equal."); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return Append (messages, flags, cancellationToken, progress); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); + return ReplaceAsync (FormatOptions.Default, uid, request, cancellationToken); } /// - /// Append the specified messages to the folder. + /// Replace a message in the folder. /// /// - /// Appends the specified messages to the folder and returns the UniqueIds assigned to the messages. + /// Replaces a message in the folder and returns the UniqueId assigned to the new message. /// - /// The UIDs of the appended messages, if available; otherwise an empty array. - /// The array of messages to append to the folder. - /// The message flags to use for each of the messages. - /// The received dates to use for each of the messages. + /// The UID of the new message, if available; otherwise, . + /// The formatting options. + /// The UID of the message to be replaced. + /// The replace request. /// The cancellation token. - /// The progress reporting mechanism. /// - /// is null. - /// -or- - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// - /// One or more of the is null. + /// is invalid. /// -or- - /// The number of messages, flags, and dates do not match. + /// The destination folder does not belong to this . /// /// /// The has been disposed. @@ -3533,12 +3390,21 @@ public int Count { /// /// The is not authenticated. /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// /// /// The does not exist. /// + /// + /// The is not currently open in read-write mode. + /// /// /// The operation was canceled via the cancellation token. /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// /// /// An I/O error occurred. /// @@ -3546,36 +3412,30 @@ public int Count { /// The server's response contained unexpected tokens. /// /// - /// The command failed. + /// The server replied with a NO or BAD response. /// - public virtual IList Append (IList messages, IList flags, IList dates, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) - { - return Append (FormatOptions.Default, messages, flags, dates, cancellationToken, progress); - } + public abstract UniqueId? Replace (FormatOptions options, UniqueId uid, IReplaceRequest request, CancellationToken cancellationToken = default); /// - /// Asynchronously append the specified messages to the folder. + /// Asynchronously replace a message in the folder. /// /// - /// Asynchronously appends the specified messages to the folder and returns the UniqueIds assigned to the messages. + /// Asynchronously replaces a message in the folder and returns the UniqueId assigned to the new message. /// - /// The UIDs of the appended messages, if available; otherwise an empty array. - /// The array of messages to append to the folder. - /// The message flags to use for each of the messages. - /// The received dates to use for each of the messages. + /// The UID of the new message, if available; otherwise, . + /// The formatting options. + /// The UID of the message to be replaced. + /// The replace request. /// The cancellation token. - /// The progress reporting mechanism. /// - /// is null. + /// is . /// -or- - /// is null. - /// -or- - /// is null. + /// is . /// /// - /// One or more of the is null. + /// is invalid. /// -or- - /// The number of messages, flags, and dates do not match. + /// The destination folder does not belong to this . /// /// /// The has been disposed. @@ -3586,12 +3446,21 @@ public int Count { /// /// The is not authenticated. /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// /// /// The does not exist. /// + /// + /// The is not currently open in read-write mode. + /// /// /// The operation was canceled via the cancellation token. /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// /// /// An I/O error occurred. /// @@ -3599,57 +3468,28 @@ public int Count { /// The server's response contained unexpected tokens. /// /// - /// The command failed. + /// The server replied with a NO or BAD response. /// - public virtual Task> AppendAsync (IList messages, IList flags, IList dates, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) - { - if (messages == null) - throw new ArgumentNullException (nameof (messages)); - - for (int i = 0; i < messages.Count; i++) { - if (messages[i] == null) - throw new ArgumentException ("One or more of the messages is null."); - } - - if (flags == null) - throw new ArgumentNullException (nameof (flags)); - - if (dates == null) - throw new ArgumentNullException (nameof (dates)); - - if (messages.Count != flags.Count || messages.Count != dates.Count) - throw new ArgumentException ("The number of messages, the number of flags, and the number of dates must be equal."); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return Append (messages, flags, dates, cancellationToken, progress); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task ReplaceAsync (FormatOptions options, UniqueId uid, IReplaceRequest request, CancellationToken cancellationToken = default); /// - /// Append the specified messages to the folder. + /// Replace a message in the folder. /// /// - /// Appends the specified messages to the folder and returns the UniqueIds assigned to the messages. + /// Replaces a message in the folder and returns the UniqueId assigned to the new message. /// - /// The UIDs of the appended messages, if available; otherwise an empty array. - /// The formatting options. - /// The array of messages to append to the folder. - /// The message flags to use for each message. + /// The UID of the new message, if available; otherwise, . + /// The index of the message to be replaced. + /// The replace request. /// The cancellation token. - /// The progress reporting mechanism. /// - /// is null. - /// -or- - /// is null. - /// -or- - /// is null. + /// is . + /// + /// + /// is out of range. /// /// - /// One or more of the is null. - /// -or- - /// The number of messages does not match the number of flags. + /// The destination folder does not belong to this . /// /// /// The has been disposed. @@ -3660,18 +3500,18 @@ public int Count { /// /// The is not authenticated. /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// /// /// The does not exist. /// - /// - /// Internationalized formatting was requested but has not been enabled. + /// + /// The is not currently open in read-write mode. /// /// /// The operation was canceled via the cancellation token. /// - /// - /// Internationalized formatting was requested but is not supported by the server. - /// /// /// An I/O error occurred. /// @@ -3679,33 +3519,31 @@ public int Count { /// The server's response contained unexpected tokens. /// /// - /// The command failed. + /// The server replied with a NO or BAD response. /// - public abstract IList Append (FormatOptions options, IList messages, IList flags, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + public virtual UniqueId? Replace (int index, IReplaceRequest request, CancellationToken cancellationToken = default) + { + return Replace (FormatOptions.Default, index, request, cancellationToken); + } /// - /// Asynchronously append the specified messages to the folder. + /// Asynchronously replace a message in the folder. /// /// - /// Asynchronously appends the specified messages to the folder and returns the UniqueIds assigned to the messages. + /// Asynchronously replaces a message in the folder and returns the UniqueId assigned to the new message. /// - /// The UIDs of the appended messages, if available; otherwise an empty array. - /// The formatting options. - /// The array of messages to append to the folder. - /// The message flags to use for each message. + /// The UID of the appended message, if available; otherwise, . + /// The index of the message to be replaced. + /// The replace request. /// The cancellation token. - /// The progress reporting mechanism. /// - /// is null. - /// -or- - /// is null. - /// -or- - /// is null. + /// is . + /// + /// + /// is out of range. /// /// - /// One or more of the is null. - /// -or- - /// The number of messages does not match the number of flags. + /// The destination folder does not belong to this . /// /// /// The has been disposed. @@ -3716,18 +3554,18 @@ public int Count { /// /// The is not authenticated. /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// /// /// The does not exist. /// - /// - /// Internationalized formatting was requested but has not been enabled. + /// + /// The is not currently open in read-write mode. /// /// /// The operation was canceled via the cancellation token. /// - /// - /// Internationalized formatting was requested but is not supported by the server. - /// /// /// An I/O error occurred. /// @@ -3735,60 +3573,34 @@ public int Count { /// The server's response contained unexpected tokens. /// /// - /// The command failed. + /// The server replied with a NO or BAD response. /// - public virtual Task> AppendAsync (FormatOptions options, IList messages, IList flags, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + public virtual Task ReplaceAsync (int index, IReplaceRequest request, CancellationToken cancellationToken = default) { - if (options == null) - throw new ArgumentNullException (nameof (options)); - - if (messages == null) - throw new ArgumentNullException (nameof (messages)); - - for (int i = 0; i < messages.Count; i++) { - if (messages[i] == null) - throw new ArgumentException ("One or more of the messages is null."); - } - - if (flags == null) - throw new ArgumentNullException (nameof (flags)); - - if (messages.Count != flags.Count) - throw new ArgumentException ("The number of messages and the number of flags must be equal."); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return Append (options, messages, flags, cancellationToken, progress); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); + return ReplaceAsync (FormatOptions.Default, index, request, cancellationToken); } /// - /// Append the specified messages to the folder. + /// Replace a message in the folder. /// /// - /// Appends the specified messages to the folder and returns the UniqueIds assigned to the messages. + /// Replaces a message in the folder and returns the UniqueId assigned to the new message. /// - /// The UIDs of the appended messages, if available; otherwise an empty array. + /// The UID of the new message, if available; otherwise, . /// The formatting options. - /// The array of messages to append to the folder. - /// The message flags to use for each of the messages. - /// The received dates to use for each of the messages. + /// The index of the message to be replaced. + /// The replace request. /// The cancellation token. - /// The progress reporting mechanism. /// - /// is null. - /// -or- - /// is null. - /// -or- - /// is null. + /// is . /// -or- - /// is null. + /// is . + /// + /// + /// is out of range. /// /// - /// One or more of the is null. - /// -or- - /// The number of messages, flags, and dates do not match. + /// The destination folder does not belong to this . /// /// /// The has been disposed. @@ -3799,17 +3611,20 @@ public int Count { /// /// The is not authenticated. /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// /// /// The does not exist. /// - /// - /// Internationalized formatting was requested but has not been enabled. + /// + /// The is not currently open in read-write mode. /// /// /// The operation was canceled via the cancellation token. /// /// - /// Internationalized formatting was requested but is not supported by the server. + /// Internationalized formatting was requested but is not supported by the server. /// /// /// An I/O error occurred. @@ -3818,36 +3633,31 @@ public int Count { /// The server's response contained unexpected tokens. /// /// - /// The command failed. + /// The server replied with a NO or BAD response. /// - public abstract IList Append (FormatOptions options, IList messages, IList flags, IList dates, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + public abstract UniqueId? Replace (FormatOptions options, int index, IReplaceRequest request, CancellationToken cancellationToken = default); /// - /// Asynchronously append the specified messages to the folder. + /// Asynchronously replace a message in the folder. /// /// - /// Asynchronously appends the specified messages to the folder and returns the UniqueIds assigned to the messages. + /// Asynchronously replaces a message in the folder and returns the UniqueId assigned to the new message. /// - /// The UIDs of the appended messages, if available; otherwise an empty array. + /// The UID of the new message, if available; otherwise, . /// The formatting options. - /// The array of messages to append to the folder. - /// The message flags to use for each of the messages. - /// The received dates to use for each of the messages. + /// The index of the message to be replaced. + /// The replace request. /// The cancellation token. - /// The progress reporting mechanism. /// - /// is null. - /// -or- - /// is null. + /// is . /// -or- - /// is null. - /// -or- - /// is null. + /// is . + /// + /// + /// is out of range. /// /// - /// One or more of the is null. - /// -or- - /// The number of messages, flags, and dates do not match. + /// The destination folder does not belong to this . /// /// /// The has been disposed. @@ -3858,17 +3668,20 @@ public int Count { /// /// The is not authenticated. /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// /// /// The does not exist. /// - /// - /// Internationalized formatting was requested but has not been enabled. + /// + /// The is not currently open in read-write mode. /// /// /// The operation was canceled via the cancellation token. /// /// - /// Internationalized formatting was requested but is not supported by the server. + /// Internationalized formatting was requested but is not supported by the server. /// /// /// An I/O error occurred. @@ -3877,36 +3690,9 @@ public int Count { /// The server's response contained unexpected tokens. /// /// - /// The command failed. + /// The server replied with a NO or BAD response. /// - public virtual Task> AppendAsync (FormatOptions options, IList messages, IList flags, IList dates, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) - { - if (options == null) - throw new ArgumentNullException (nameof (options)); - - if (messages == null) - throw new ArgumentNullException (nameof (messages)); - - for (int i = 0; i < messages.Count; i++) { - if (messages[i] == null) - throw new ArgumentException ("One or more of the messages is null."); - } - - if (flags == null) - throw new ArgumentNullException (nameof (flags)); - - if (dates == null) - throw new ArgumentNullException (nameof (dates)); - - if (messages.Count != flags.Count || messages.Count != dates.Count) - throw new ArgumentException ("The number of messages, the number of flags, and the number of dates must be equal."); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return Append (options, messages, flags, dates, cancellationToken, progress); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task ReplaceAsync (FormatOptions options, int index, IReplaceRequest request, CancellationToken cancellationToken = default); /// /// Copy the specified message to the destination folder. @@ -3914,12 +3700,12 @@ public int Count { /// /// Copies the specified message to the destination folder. /// - /// The UID of the message in the destination folder, if available; otherwise, null. + /// The UID of the message in the destination folder, if available; otherwise, . /// The UID of the message to copy. /// The destination folder. /// The cancellation token. /// - /// is null. + /// is . /// /// /// is invalid. @@ -3953,11 +3739,8 @@ public int Count { /// /// The command failed. /// - public virtual UniqueId? CopyTo (UniqueId uid, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)) + public virtual UniqueId? CopyTo (UniqueId uid, IMailFolder destination, CancellationToken cancellationToken = default) { - if (destination == null) - throw new ArgumentNullException (nameof (destination)); - var uids = CopyTo (new [] { uid }, destination, cancellationToken); if (uids != null && uids.Destination.Count > 0) @@ -3972,12 +3755,12 @@ public int Count { /// /// Asynchronously copies the specified message to the destination folder. /// - /// The UID of the message in the destination folder, if available; otherwise, null. + /// The UID of the message in the destination folder, if available; otherwise, . /// The UID of the message to copy. /// The destination folder. /// The cancellation token. /// - /// is null. + /// is . /// /// /// is invalid. @@ -4011,16 +3794,14 @@ public int Count { /// /// The command failed. /// - public virtual Task CopyToAsync (UniqueId uid, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)) + public virtual async Task CopyToAsync (UniqueId uid, IMailFolder destination, CancellationToken cancellationToken = default) { - if (destination == null) - throw new ArgumentNullException (nameof (destination)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return CopyTo (uid, destination, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); + var uids = await CopyToAsync (new [] { uid }, destination, cancellationToken).ConfigureAwait (false); + + if (uids != null && uids.Destination.Count > 0) + return uids.Destination[0]; + + return null; } /// @@ -4034,9 +3815,9 @@ public int Count { /// The destination folder. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// One or more of the is invalid. @@ -4070,7 +3851,7 @@ public int Count { /// /// The command failed. /// - public abstract UniqueIdMap CopyTo (IList uids, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)); + public abstract UniqueIdMap CopyTo (IList uids, IMailFolder destination, CancellationToken cancellationToken = default); /// /// Asynchronously copy the specified messages to the destination folder. @@ -4083,9 +3864,9 @@ public int Count { /// The destination folder. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// One or more of the is invalid. @@ -4119,20 +3900,7 @@ public int Count { /// /// The command failed. /// - public virtual Task CopyToAsync (IList uids, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)) - { - if (uids == null) - throw new ArgumentNullException (nameof (uids)); - - if (destination == null) - throw new ArgumentNullException (nameof (destination)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return CopyTo (uids, destination, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task CopyToAsync (IList uids, IMailFolder destination, CancellationToken cancellationToken = default); /// /// Move the specified message to the destination folder. @@ -4140,12 +3908,12 @@ public int Count { /// /// Moves the specified message to the destination folder. /// - /// The UID of the message in the destination folder, if available; otherwise, null. + /// The UID of the message in the destination folder, if available; otherwise, . /// The UID of the message to move. /// The destination folder. /// The cancellation token. /// - /// is null. + /// is . /// /// /// is invalid. @@ -4179,11 +3947,8 @@ public int Count { /// /// The command failed. /// - public virtual UniqueId? MoveTo (UniqueId uid, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)) + public virtual UniqueId? MoveTo (UniqueId uid, IMailFolder destination, CancellationToken cancellationToken = default) { - if (destination == null) - throw new ArgumentNullException (nameof (destination)); - var uids = MoveTo (new [] { uid }, destination, cancellationToken); if (uids != null && uids.Destination.Count > 0) @@ -4198,12 +3963,12 @@ public int Count { /// /// Asynchronously moves the specified message to the destination folder. /// - /// The UID of the message in the destination folder, if available; otherwise, null. + /// The UID of the message in the destination folder, if available; otherwise, . /// The UID of the message to move. /// The destination folder. /// The cancellation token. /// - /// is null. + /// is . /// /// /// is invalid. @@ -4237,16 +4002,14 @@ public int Count { /// /// The command failed. /// - public virtual Task MoveToAsync (UniqueId uid, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)) + public virtual async Task MoveToAsync (UniqueId uid, IMailFolder destination, CancellationToken cancellationToken = default) { - if (destination == null) - throw new ArgumentNullException (nameof (destination)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return MoveTo (uid, destination, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); + var uids = await MoveToAsync (new [] { uid }, destination, cancellationToken).ConfigureAwait (false); + + if (uids != null && uids.Destination.Count > 0) + return uids.Destination[0]; + + return null; } /// @@ -4260,9 +4023,9 @@ public int Count { /// The destination folder. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// One or more of the is invalid. @@ -4296,7 +4059,7 @@ public int Count { /// /// The command failed. /// - public abstract UniqueIdMap MoveTo (IList uids, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)); + public abstract UniqueIdMap MoveTo (IList uids, IMailFolder destination, CancellationToken cancellationToken = default); /// /// Asynchronously move the specified messages to the destination folder. @@ -4309,9 +4072,9 @@ public int Count { /// The destination folder. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// One or more of the is invalid. @@ -4345,20 +4108,7 @@ public int Count { /// /// The command failed. /// - public virtual Task MoveToAsync (IList uids, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)) - { - if (uids == null) - throw new ArgumentNullException (nameof (uids)); - - if (destination == null) - throw new ArgumentNullException (nameof (destination)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return MoveTo (uids, destination, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task MoveToAsync (IList uids, IMailFolder destination, CancellationToken cancellationToken = default); /// /// Copy the specified message to the destination folder. @@ -4370,7 +4120,7 @@ public int Count { /// The destination folder. /// The cancellation token. /// - /// is null. + /// is . /// /// /// does not refer to a valid message index. @@ -4402,14 +4152,11 @@ public int Count { /// /// The command failed. /// - public virtual void CopyTo (int index, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)) + public virtual void CopyTo (int index, IMailFolder destination, CancellationToken cancellationToken = default) { if (index < 0 || index >= Count) throw new ArgumentOutOfRangeException (nameof (index)); - if (destination == null) - throw new ArgumentNullException (nameof (destination)); - CopyTo (new [] { index }, destination, cancellationToken); } @@ -4424,7 +4171,7 @@ public int Count { /// The destination folder. /// The cancellation token. /// - /// is null. + /// is . /// /// /// does not refer to a valid message index. @@ -4456,14 +4203,11 @@ public int Count { /// /// The command failed. /// - public virtual Task CopyToAsync (int index, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)) + public virtual Task CopyToAsync (int index, IMailFolder destination, CancellationToken cancellationToken = default) { if (index < 0 || index >= Count) throw new ArgumentOutOfRangeException (nameof (index)); - if (destination == null) - throw new ArgumentNullException (nameof (destination)); - return CopyToAsync (new [] { index }, destination, cancellationToken); } @@ -4477,9 +4221,9 @@ public int Count { /// The destination folder. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// One or more of the is invalid. @@ -4510,7 +4254,7 @@ public int Count { /// /// The command failed. /// - public abstract void CopyTo (IList indexes, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)); + public abstract void CopyTo (IList indexes, IMailFolder destination, CancellationToken cancellationToken = default); /// /// Asynchronously copy the specified messages to the destination folder. @@ -4523,9 +4267,9 @@ public int Count { /// The destination folder. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// One or more of the is invalid. @@ -4556,20 +4300,7 @@ public int Count { /// /// The command failed. /// - public virtual Task CopyToAsync (IList indexes, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)) - { - if (indexes == null) - throw new ArgumentNullException (nameof (indexes)); - - if (destination == null) - throw new ArgumentNullException (nameof (destination)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - CopyTo (indexes, destination, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task CopyToAsync (IList indexes, IMailFolder destination, CancellationToken cancellationToken = default); /// /// Move the specified message to the destination folder. @@ -4581,7 +4312,7 @@ public int Count { /// The destination folder. /// The cancellation token. /// - /// is null. + /// is . /// /// /// does not refer to a valid message index. @@ -4613,14 +4344,11 @@ public int Count { /// /// The command failed. /// - public virtual void MoveTo (int index, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)) + public virtual void MoveTo (int index, IMailFolder destination, CancellationToken cancellationToken = default) { if (index < 0 || index >= Count) throw new ArgumentOutOfRangeException (nameof (index)); - if (destination == null) - throw new ArgumentNullException (nameof (destination)); - MoveTo (new [] { index }, destination, cancellationToken); } @@ -4635,7 +4363,7 @@ public int Count { /// The destination folder. /// The cancellation token. /// - /// is null. + /// is . /// /// /// does not refer to a valid message index. @@ -4667,14 +4395,11 @@ public int Count { /// /// The command failed. /// - public virtual Task MoveToAsync (int index, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)) + public virtual Task MoveToAsync (int index, IMailFolder destination, CancellationToken cancellationToken = default) { if (index < 0 || index >= Count) throw new ArgumentOutOfRangeException (nameof (index)); - if (destination == null) - throw new ArgumentNullException (nameof (destination)); - return MoveToAsync (new [] { index }, destination, cancellationToken); } @@ -4688,9 +4413,9 @@ public int Count { /// The destination folder. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// One or more of the is invalid. @@ -4721,7 +4446,7 @@ public int Count { /// /// The command failed. /// - public abstract void MoveTo (IList indexes, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)); + public abstract void MoveTo (IList indexes, IMailFolder destination, CancellationToken cancellationToken = default); /// /// Asynchronously move the specified messages to the destination folder. @@ -4734,9 +4459,9 @@ public int Count { /// The destination folder. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// One or more of the is invalid. @@ -4767,20 +4492,7 @@ public int Count { /// /// The command failed. /// - public virtual Task MoveToAsync (IList indexes, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)) - { - if (indexes == null) - throw new ArgumentNullException (nameof (indexes)); - - if (destination == null) - throw new ArgumentNullException (nameof (destination)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - MoveTo (indexes, destination, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task MoveToAsync (IList indexes, IMailFolder destination, CancellationToken cancellationToken = default); /// /// Fetch the message summaries for the specified message UIDs. @@ -4795,17 +4507,16 @@ public int Count { /// not requested at all. /// /// - /// + /// /// /// An enumeration of summaries for the requested messages. /// The UIDs. - /// The message summary items to fetch. + /// The fetch request. /// The cancellation token. /// - /// is null. - /// - /// - /// is empty. + /// is . + /// -or- + /// is . /// /// /// One or more of the is invalid. @@ -4834,7 +4545,7 @@ public int Count { /// /// The command failed. /// - public abstract IList Fetch (IList uids, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)); + public abstract IList Fetch (IList uids, IFetchRequest request, CancellationToken cancellationToken = default); /// /// Asynchronously fetch the message summaries for the specified message UIDs. @@ -4851,13 +4562,12 @@ public int Count { /// /// An enumeration of summaries for the requested messages. /// The UIDs. - /// The message summary items to fetch. + /// The fetch request. /// The cancellation token. /// - /// is null. - /// - /// - /// is empty. + /// is . + /// -or- + /// is . /// /// /// One or more of the is invalid. @@ -4886,26 +4596,13 @@ public int Count { /// /// The command failed. /// - public virtual Task> FetchAsync (IList uids, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)) - { - if (uids == null) - throw new ArgumentNullException (nameof (uids)); - - if (items == MessageSummaryItems.None) - throw new ArgumentOutOfRangeException (nameof (items)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return Fetch (uids, items, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task> FetchAsync (IList uids, IFetchRequest request, CancellationToken cancellationToken = default); /// - /// Fetch the message summaries for the specified message UIDs. + /// Fetch the message summaries for the specified message indexes. /// /// - /// Fetches the message summaries for the specified message UIDs. + /// Fetches the message summaries for the specified message indexes. /// It should be noted that if another client has modified any message /// in the folder, the mail service may choose to return information that was /// not explicitly requested. It is therefore important to be prepared to @@ -4914,19 +4611,16 @@ public int Count { /// not requested at all. /// /// An enumeration of summaries for the requested messages. - /// The UIDs. - /// The message summary items to fetch. - /// The desired header fields. + /// The indexes. + /// The fetch request. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// - /// One or more of the is invalid. - /// -or- - /// is empty. + /// One or more of the is invalid. /// /// /// The has been disposed. @@ -4952,14 +4646,14 @@ public int Count { /// /// The command failed. /// - public abstract IList Fetch (IList uids, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)); + public abstract IList Fetch (IList indexes, IFetchRequest request, CancellationToken cancellationToken = default); /// - /// Asynchronously fetch the message summaries for the specified message UIDs. + /// Asynchronously fetch the message summaries for the specified message indexes. /// /// /// Asynchronously fetches the message summaries for the specified message - /// UIDs. + /// indexes. /// It should be noted that if another client has modified any message /// in the folder, the mail service may choose to return information that was /// not explicitly requested. It is therefore important to be prepared to @@ -4968,19 +4662,16 @@ public int Count { /// not requested at all. /// /// An enumeration of summaries for the requested messages. - /// The UIDs. - /// The message summary items to fetch. - /// The desired header fields. + /// The indexes. + /// The fetch request. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// - /// One or more of the is invalid. - /// -or- - /// is empty. + /// One or more of the is invalid. /// /// /// The has been disposed. @@ -5006,29 +4697,14 @@ public int Count { /// /// The command failed. /// - public virtual Task> FetchAsync (IList uids, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)) - { - if (uids == null) - throw new ArgumentNullException (nameof (uids)); - - if (fields == null) - throw new ArgumentNullException (nameof (fields)); - - if (fields.Count == 0) - throw new ArgumentException ("The set of header fields cannot be empty.", nameof (fields)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return Fetch (uids, items, fields, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task> FetchAsync (IList indexes, IFetchRequest request, CancellationToken cancellationToken = default); /// - /// Fetch the message summaries for the specified message UIDs. + /// Fetch the message summaries for the messages between the two indexes, inclusive. /// /// - /// Fetches the message summaries for the specified message UIDs. + /// Fetches the message summaries for the messages between the two + /// indexes, inclusive. /// It should be noted that if another client has modified any message /// in the folder, the mail service may choose to return information that was /// not explicitly requested. It is therefore important to be prepared to @@ -5037,19 +4713,17 @@ public int Count { /// not requested at all. /// /// An enumeration of summaries for the requested messages. - /// The UIDs. - /// The message summary items to fetch. - /// The desired header fields. + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// The fetch request. /// The cancellation token. /// - /// is null. - /// -or- - /// is null. + /// is . /// - /// - /// One or more of the is invalid. + /// + /// is out of range. /// -or- - /// is empty. + /// is out of range. /// /// /// The has been disposed. @@ -5075,14 +4749,14 @@ public int Count { /// /// The command failed. /// - public abstract IList Fetch (IList uids, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)); + public abstract IList Fetch (int min, int max, IFetchRequest request, CancellationToken cancellationToken = default); /// - /// Asynchronously fetch the message summaries for the specified message UIDs. + /// Asynchronously fetch the message summaries for the messages between the two indexes, inclusive. /// /// - /// Asynchronously fetches the message summaries for the specified message - /// UIDs. + /// Asynchronously fetches the message summaries for the messages between + /// the two indexes, inclusive. /// It should be noted that if another client has modified any message /// in the folder, the mail service may choose to return information that was /// not explicitly requested. It is therefore important to be prepared to @@ -5091,19 +4765,17 @@ public int Count { /// not requested at all. /// /// An enumeration of summaries for the requested messages. - /// The UIDs. - /// The message summary items to fetch. - /// The desired header fields. + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// The fetch request. /// The cancellation token. /// - /// is null. - /// -or- - /// is null. + /// is . /// - /// - /// One or more of the is invalid. + /// + /// is out of range. /// -or- - /// is empty. + /// is out of range. /// /// /// The has been disposed. @@ -5129,55 +4801,20 @@ public int Count { /// /// The command failed. /// - public virtual Task> FetchAsync (IList uids, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)) - { - if (uids == null) - throw new ArgumentNullException (nameof (uids)); - - if (fields == null) - throw new ArgumentNullException (nameof (fields)); - - if (fields.Count == 0) - throw new ArgumentException ("The set of header fields cannot be empty.", nameof (fields)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return Fetch (uids, items, fields, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task> FetchAsync (int min, int max, IFetchRequest request, CancellationToken cancellationToken = default); /// - /// Fetch the message summaries for the specified message UIDs that have a - /// higher mod-sequence value than the one specified. + /// Get the specified message headers. /// /// - /// Fetches the message summaries for the specified message UIDs that - /// have a higher mod-sequence value than the one specified. - /// If the mail store supports quick resynchronization and the application has - /// enabled this feature via , - /// then this method will emit events for messages that - /// have vanished since the specified mod-sequence value. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. + /// Gets the specified message headers. /// - /// An enumeration of summaries for the requested messages. - /// The UIDs. - /// The mod-sequence value. - /// The message summary items to fetch. + /// The message headers. + /// The UID of the message. /// The cancellation token. - /// - /// is null. - /// - /// - /// is empty. - /// + /// The progress reporting mechanism. /// - /// One or more of the is invalid. + /// is invalid. /// /// /// The has been disposed. @@ -5191,8 +4828,8 @@ public int Count { /// /// The folder is not currently open. /// - /// - /// The does not support mod-sequences. + /// + /// The did not return the requested message headers. /// /// /// The operation was canceled via the cancellation token. @@ -5206,39 +4843,20 @@ public int Count { /// /// The command failed. /// - public abstract IList Fetch (IList uids, ulong modseq, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)); + public abstract HeaderList GetHeaders (UniqueId uid, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Asynchronously fetch the message summaries for the specified message UIDs that have a - /// higher mod-sequence value than the one specified. + /// Asynchronously get the specified message headers. /// /// - /// Asynchronously fetches the message summaries for the specified message UIDs that - /// have a higher mod-sequence value than the one specified. - /// If the mail store supports quick resynchronization and the application has - /// enabled this feature via , - /// then this method will emit events for messages that - /// have vanished since the specified mod-sequence value. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. + /// Asynchronously gets the specified message headers. /// - /// An enumeration of summaries for the requested messages. - /// The UIDs. - /// The mod-sequence value. - /// The message summary items to fetch. + /// The message headers. + /// The UID of the message. /// The cancellation token. - /// - /// is null. - /// - /// - /// is empty. - /// + /// The progress reporting mechanism. /// - /// One or more of the is invalid. + /// is invalid. /// /// /// The has been disposed. @@ -5252,8 +4870,8 @@ public int Count { /// /// The folder is not currently open. /// - /// - /// The does not support mod-sequences. + /// + /// The did not return the requested message headers. /// /// /// The operation was canceled via the cancellation token. @@ -5267,54 +4885,24 @@ public int Count { /// /// The command failed. /// - public virtual Task> FetchAsync (IList uids, ulong modseq, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)) - { - if (uids == null) - throw new ArgumentNullException (nameof (uids)); - - if (items == MessageSummaryItems.None) - throw new ArgumentOutOfRangeException (nameof (items)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return Fetch (uids, modseq, items, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task GetHeadersAsync (UniqueId uid, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Fetch the message summaries for the specified message UIDs that have a - /// higher mod-sequence value than the one specified. + /// Get the specified body part headers. /// /// - /// Fetches the message summaries for the specified message UIDs that - /// have a higher mod-sequence value than the one specified. - /// If the mail store supports quick resynchronization and the application has - /// enabled this feature via , - /// then this method will emit events for messages that - /// have vanished since the specified mod-sequence value. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. + /// Gets the specified body part headers. /// - /// An enumeration of summaries for the requested messages. - /// The UIDs. - /// The mod-sequence value. - /// The message summary items to fetch. - /// The desired header fields. + /// The body part headers. + /// The UID of the message. + /// The body part. /// The cancellation token. + /// The progress reporting mechanism. /// - /// is null. - /// -or- - /// is null. + /// is . /// /// - /// One or more of the is invalid. - /// -or- - /// is empty. + /// is invalid. /// /// /// The has been disposed. @@ -5328,8 +4916,8 @@ public int Count { /// /// The folder is not currently open. /// - /// - /// The does not support mod-sequences. + /// + /// The did not return the requested body part headers. /// /// /// The operation was canceled via the cancellation token. @@ -5343,41 +4931,24 @@ public int Count { /// /// The command failed. /// - public abstract IList Fetch (IList uids, ulong modseq, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)); + public abstract HeaderList GetHeaders (UniqueId uid, BodyPart part, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Asynchronously fetch the message summaries for the specified message UIDs that have a - /// higher mod-sequence value than the one specified. + /// Asynchronously get the specified body part headers. /// /// - /// Asynchronously fetches the message summaries for the specified message UIDs that - /// have a higher mod-sequence value than the one specified. - /// If the mail store supports quick resynchronization and the application has - /// enabled this feature via , - /// then this method will emit events for messages that - /// have vanished since the specified mod-sequence value. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. + /// Asynchronously gets the specified body part headers. /// - /// An enumeration of summaries for the requested messages. - /// The UIDs. - /// The mod-sequence value. - /// The message summary items to fetch. - /// The desired header fields. + /// The body part headers. + /// The UID of the message. + /// The body part. /// The cancellation token. + /// The progress reporting mechanism. /// - /// is null. - /// -or- - /// is null. + /// is . /// /// - /// One or more of the is invalid. - /// -or- - /// is empty. + /// is invalid. /// /// /// The has been disposed. @@ -5391,8 +4962,8 @@ public int Count { /// /// The folder is not currently open. /// - /// - /// The does not support mod-sequences. + /// + /// The did not return the requested body part headers. /// /// /// The operation was canceled via the cancellation token. @@ -5406,57 +4977,20 @@ public int Count { /// /// The command failed. /// - public virtual Task> FetchAsync (IList uids, ulong modseq, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)) - { - if (uids == null) - throw new ArgumentNullException (nameof (uids)); - - if (fields == null) - throw new ArgumentNullException (nameof (fields)); - - if (fields.Count == 0) - throw new ArgumentException ("The set of header fields cannot be empty.", nameof (fields)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return Fetch (uids, modseq, items, fields, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task GetHeadersAsync (UniqueId uid, BodyPart part, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Fetch the message summaries for the specified message UIDs that have a - /// higher mod-sequence value than the one specified. + /// Get the specified message headers. /// /// - /// Fetches the message summaries for the specified message UIDs that - /// have a higher mod-sequence value than the one specified. - /// If the mail store supports quick resynchronization and the application has - /// enabled this feature via , - /// then this method will emit events for messages that - /// have vanished since the specified mod-sequence value. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. + /// Gets the specified message headers. /// - /// An enumeration of summaries for the requested messages. - /// The UIDs. - /// The mod-sequence value. - /// The message summary items to fetch. - /// The desired header fields. + /// The message headers. + /// The index of the message. /// The cancellation token. - /// - /// is null. - /// -or- - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// is empty. + /// The progress reporting mechanism. + /// + /// is out of range. /// /// /// The has been disposed. @@ -5470,8 +5004,8 @@ public int Count { /// /// The folder is not currently open. /// - /// - /// The does not support mod-sequences. + /// + /// The did not return the requested message headers. /// /// /// The operation was canceled via the cancellation token. @@ -5485,41 +5019,20 @@ public int Count { /// /// The command failed. /// - public abstract IList Fetch (IList uids, ulong modseq, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)); + public abstract HeaderList GetHeaders (int index, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Asynchronously fetch the message summaries for the specified message UIDs that have a - /// higher mod-sequence value than the one specified. + /// Asynchronously get the specified message headers. /// /// - /// Asynchronously fetches the message summaries for the specified message UIDs that - /// have a higher mod-sequence value than the one specified. - /// If the mail store supports quick resynchronization and the application has - /// enabled this feature via , - /// then this method will emit events for messages that - /// have vanished since the specified mod-sequence value. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. + /// Asynchronously gets the specified message headers. /// - /// An enumeration of summaries for the requested messages. - /// The UIDs. - /// The mod-sequence value. - /// The message summary items to fetch. - /// The desired header fields. + /// The message headers. + /// The index of the message. /// The cancellation token. - /// - /// is null. - /// -or- - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// is empty. + /// The progress reporting mechanism. + /// + /// is out of range. /// /// /// The has been disposed. @@ -5533,8 +5046,8 @@ public int Count { /// /// The folder is not currently open. /// - /// - /// The does not support mod-sequences. + /// + /// The did not return the requested message headers. /// /// /// The operation was canceled via the cancellation token. @@ -5548,48 +5061,24 @@ public int Count { /// /// The command failed. /// - public virtual Task> FetchAsync (IList uids, ulong modseq, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)) - { - if (uids == null) - throw new ArgumentNullException (nameof (uids)); - - if (fields == null) - throw new ArgumentNullException (nameof (fields)); - - if (fields.Count == 0) - throw new ArgumentException ("The set of header fields cannot be empty.", nameof (fields)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return Fetch (uids, modseq, items, fields, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task GetHeadersAsync (int index, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Fetch the message summaries for the specified message indexes. + /// Get the specified body part headers. /// /// - /// Fetches the message summaries for the specified message indexes. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. + /// Gets the specified body part headers. /// - /// An enumeration of summaries for the requested messages. - /// The indexes. - /// The message summary items to fetch. + /// The body part headers. + /// The index of the message. + /// The body part. /// The cancellation token. - /// - /// is null. - /// + /// The progress reporting mechanism. /// - /// is empty. + /// is out of range. /// - /// - /// One or more of the is invalid. + /// + /// is . /// /// /// The has been disposed. @@ -5603,6 +5092,9 @@ public int Count { /// /// The folder is not currently open. /// + /// + /// The did not return the requested body part headers. + /// /// /// The operation was canceled via the cancellation token. /// @@ -5615,33 +5107,24 @@ public int Count { /// /// The command failed. /// - public abstract IList Fetch (IList indexes, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)); + public abstract HeaderList GetHeaders (int index, BodyPart part, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Asynchronously fetch the message summaries for the specified message indexes. + /// Asynchronously get the specified body part headers. /// /// - /// Asynchronously fetches the message summaries for the specified message - /// indexes. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. + /// Asynchronously gets the specified body part headers. /// - /// An enumeration of summaries for the requested messages. - /// The indexes. - /// The message summary items to fetch. + /// The body part headers. + /// The index of the message. + /// The body part. /// The cancellation token. - /// - /// is null. - /// + /// The progress reporting mechanism. /// - /// is empty. + /// is out of range. /// - /// - /// One or more of the is invalid. + /// + /// is . /// /// /// The has been disposed. @@ -5655,6 +5138,9 @@ public int Count { /// /// The folder is not currently open. /// + /// + /// The did not return the requested body part headers. + /// /// /// The operation was canceled via the cancellation token. /// @@ -5667,47 +5153,23 @@ public int Count { /// /// The command failed. /// - public virtual Task> FetchAsync (IList indexes, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)) - { - if (indexes == null) - throw new ArgumentNullException (nameof (indexes)); - - if (items == MessageSummaryItems.None) - throw new ArgumentOutOfRangeException (nameof (items)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return Fetch (indexes, items, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task GetHeadersAsync (int index, BodyPart part, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Fetch the message summaries for the specified message indexes. + /// Get the specified message. /// /// - /// Fetches the message summaries for the specified message indexes. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. + /// Gets the specified message. /// - /// An enumeration of summaries for the requested messages. - /// The indexes. - /// The message summary items to fetch. - /// The desired header fields. + /// + /// + /// + /// The message. + /// The UID of the message. /// The cancellation token. - /// - /// is null. - /// -or- - /// is null. - /// + /// The progress reporting mechanism. /// - /// One or more of the is invalid. - /// -or- - /// is empty. + /// is invalid. /// /// /// The has been disposed. @@ -5721,6 +5183,9 @@ public int Count { /// /// The folder is not currently open. /// + /// + /// The did not return the requested message. + /// /// /// The operation was canceled via the cancellation token. /// @@ -5733,35 +5198,23 @@ public int Count { /// /// The command failed. /// - public abstract IList Fetch (IList indexes, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)); + public abstract MimeMessage GetMessage (UniqueId uid, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Asynchronously fetch the message summaries for the specified message indexes. + /// Asynchronously get the specified message. /// /// - /// Asynchronously fetches the message summaries for the specified message - /// indexes. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. + /// Asynchronously gets the specified message. /// - /// An enumeration of summaries for the requested messages. - /// The indexes. - /// The message summary items to fetch. - /// The desired header fields. + /// + /// + /// + /// The message. + /// The UID of the message. /// The cancellation token. - /// - /// is null. - /// -or- - /// is null. - /// + /// The progress reporting mechanism. /// - /// One or more of the is invalid. - /// -or- - /// is empty. + /// is invalid. /// /// /// The has been disposed. @@ -5775,6 +5228,9 @@ public int Count { /// /// The folder is not currently open. /// + /// + /// The did not return the requested message. + /// /// /// The operation was canceled via the cancellation token. /// @@ -5787,50 +5243,23 @@ public int Count { /// /// The command failed. /// - public virtual Task> FetchAsync (IList indexes, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)) - { - if (indexes == null) - throw new ArgumentNullException (nameof (indexes)); - - if (fields == null) - throw new ArgumentNullException (nameof (fields)); - - if (fields.Count == 0) - throw new ArgumentException ("The set of header fields cannot be empty.", nameof (fields)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return Fetch (indexes, items, fields, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task GetMessageAsync (UniqueId uid, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Fetch the message summaries for the specified message indexes. + /// Get the specified message. /// /// - /// Fetches the message summaries for the specified message indexes. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. + /// Gets the specified message. /// - /// An enumeration of summaries for the requested messages. - /// The indexes. - /// The message summary items to fetch. - /// The desired header fields. + /// + /// + /// + /// The message. + /// The index of the message. /// The cancellation token. - /// - /// is null. - /// -or- - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// is empty. + /// The progress reporting mechanism. + /// + /// is out of range. /// /// /// The has been disposed. @@ -5844,6 +5273,9 @@ public int Count { /// /// The folder is not currently open. /// + /// + /// The did not return the requested message. + /// /// /// The operation was canceled via the cancellation token. /// @@ -5856,35 +5288,23 @@ public int Count { /// /// The command failed. /// - public abstract IList Fetch (IList indexes, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)); + public abstract MimeMessage GetMessage (int index, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Asynchronously fetch the message summaries for the specified message indexes. + /// Asynchronously get the specified message. /// /// - /// Asynchronously fetches the message summaries for the specified message - /// indexes. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. + /// Asynchronously gets the specified message. /// - /// An enumeration of summaries for the requested messages. - /// The indexes. - /// The message summary items to fetch. - /// The desired header fields. + /// + /// + /// + /// The message. + /// The index of the message. /// The cancellation token. - /// - /// is null. - /// -or- - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// is empty. + /// The progress reporting mechanism. + /// + /// is out of range. /// /// /// The has been disposed. @@ -5898,6 +5318,9 @@ public int Count { /// /// The folder is not currently open. /// + /// + /// The did not return the requested message. + /// /// /// The operation was canceled via the cancellation token. /// @@ -5910,51 +5333,27 @@ public int Count { /// /// The command failed. /// - public virtual Task> FetchAsync (IList indexes, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)) - { - if (indexes == null) - throw new ArgumentNullException (nameof (indexes)); - - if (fields == null) - throw new ArgumentNullException (nameof (fields)); - - if (fields.Count == 0) - throw new ArgumentException ("The set of header fields cannot be empty.", nameof (fields)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return Fetch (indexes, items, fields, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task GetMessageAsync (int index, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Fetch the message summaries for the specified message indexes that have a - /// higher mod-sequence value than the one specified. + /// Get the specified body part. /// /// - /// Fetches the message summaries for the specified message indexes that - /// have a higher mod-sequence value than the one specified. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. + /// Gets the specified body part. /// - /// An enumeration of summaries for the requested messages. - /// The indexes. - /// The mod-sequence value. - /// The message summary items to fetch. + /// + /// + /// + /// The body part. + /// The UID of the message. + /// The body part. /// The cancellation token. + /// The progress reporting mechanism. /// - /// is null. - /// - /// - /// is empty. + /// is . /// /// - /// One or more of the is invalid. + /// is invalid. /// /// /// The has been disposed. @@ -5968,8 +5367,8 @@ public int Count { /// /// The folder is not currently open. /// - /// - /// The does not support mod-sequences. + /// + /// The did not return the requested message body. /// /// /// The operation was canceled via the cancellation token. @@ -5983,35 +5382,24 @@ public int Count { /// /// The command failed. /// - public abstract IList Fetch (IList indexes, ulong modseq, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)); + public abstract MimeEntity GetBodyPart (UniqueId uid, BodyPart part, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Asynchronously fetch the message summaries for the specified message indexes that - /// have a higher mod-sequence value than the one specified. + /// Asynchronously get the specified body part. /// /// - /// Asynchronously fetches the message summaries for the specified message - /// indexes that have a higher mod-sequence value than the one specified. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. + /// Asynchronously gets the specified body part. /// - /// An enumeration of summaries for the requested messages. - /// The indexes. - /// The mod-sequence value. - /// The message summary items to fetch. + /// The body part. + /// The UID of the message. + /// The body part. /// The cancellation token. + /// The progress reporting mechanism. /// - /// is null. - /// - /// - /// is empty. + /// is . /// /// - /// One or more of the is invalid. + /// is invalid. /// /// /// The has been disposed. @@ -6025,8 +5413,8 @@ public int Count { /// /// The folder is not currently open. /// - /// - /// The does not support mod-sequences. + /// + /// The did not return the requested message body. /// /// /// The operation was canceled via the cancellation token. @@ -6040,50 +5428,24 @@ public int Count { /// /// The command failed. /// - public virtual Task> FetchAsync (IList indexes, ulong modseq, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)) - { - if (indexes == null) - throw new ArgumentNullException (nameof (indexes)); - - if (items == MessageSummaryItems.None) - throw new ArgumentOutOfRangeException (nameof (items)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return Fetch (indexes, modseq, items, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task GetBodyPartAsync (UniqueId uid, BodyPart part, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Fetch the message summaries for the specified message indexes that - /// have a higher mod-sequence value than the one specified. + /// Get the specified body part. /// /// - /// Fetches the message summaries for the specified message indexes that - /// have a higher mod-sequence value than the one specified. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. + /// Gets the specified body part. /// - /// An enumeration of summaries for the requested messages. - /// The indexes. - /// The mod-sequence value. - /// The message summary items to fetch. - /// The desired header fields. + /// The body part. + /// The index of the message. + /// The body part. /// The cancellation token. + /// The progress reporting mechanism. /// - /// is null. - /// -or- - /// is null. + /// is . /// - /// - /// One or more of the is invalid. - /// -or- - /// is empty. + /// + /// is out of range. /// /// /// The has been disposed. @@ -6097,6 +5459,9 @@ public int Count { /// /// The folder is not currently open. /// + /// + /// The did not return the requested message body. + /// /// /// The operation was canceled via the cancellation token. /// @@ -6109,37 +5474,24 @@ public int Count { /// /// The command failed. /// - public abstract IList Fetch (IList indexes, ulong modseq, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)); + public abstract MimeEntity GetBodyPart (int index, BodyPart part, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Asynchronously fetch the message summaries for the specified message indexes that - /// have a higher mod-sequence value than the one specified. + /// Asynchronously get the specified body part. /// /// - /// Asynchronously fetches the message summaries for the specified message - /// indexes that have a higher mod-sequence value than the one specified. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. + /// Asynchronously gets the specified body part. /// - /// An enumeration of summaries for the requested messages. - /// The indexes. - /// The mod-sequence value. - /// The message summary items to fetch. - /// The desired header fields. + /// The body part. + /// The index of the message. + /// The body part. /// The cancellation token. + /// The progress reporting mechanism. /// - /// is null. - /// -or- - /// is null. + /// is . /// - /// - /// One or more of the is invalid. - /// -or- - /// is empty. + /// + /// is out of range. /// /// /// The has been disposed. @@ -6153,6 +5505,9 @@ public int Count { /// /// The folder is not currently open. /// + /// + /// The did not return the requested message body. + /// /// /// The operation was canceled via the cancellation token. /// @@ -6165,53 +5520,23 @@ public int Count { /// /// The command failed. /// - public virtual Task> FetchAsync (IList indexes, ulong modseq, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)) - { - if (indexes == null) - throw new ArgumentNullException (nameof (indexes)); - - if (fields == null) - throw new ArgumentNullException (nameof (fields)); - - if (fields.Count == 0) - throw new ArgumentException ("The set of header fields cannot be empty.", nameof (fields)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return Fetch (indexes, modseq, items, fields, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task GetBodyPartAsync (int index, BodyPart part, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Fetch the message summaries for the specified message indexes that - /// have a higher mod-sequence value than the one specified. + /// Get a message stream. /// /// - /// Fetches the message summaries for the specified message indexes that - /// have a higher mod-sequence value than the one specified. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. + /// Gets a message stream. /// - /// An enumeration of summaries for the requested messages. - /// The indexes. - /// The mod-sequence value. - /// The message summary items to fetch. - /// The desired header fields. + /// + /// + /// + /// The message stream. + /// The UID of the message. /// The cancellation token. - /// - /// is null. - /// -or- - /// is null. - /// + /// The progress reporting mechanism. /// - /// One or more of the is invalid. - /// -or- - /// is empty. + /// is invalid. /// /// /// The has been disposed. @@ -6225,6 +5550,9 @@ public int Count { /// /// The folder is not currently open. /// + /// + /// The did not return the requested message stream. + /// /// /// The operation was canceled via the cancellation token. /// @@ -6237,37 +5565,26 @@ public int Count { /// /// The command failed. /// - public abstract IList Fetch (IList indexes, ulong modseq, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)); + public virtual Stream GetStream (UniqueId uid, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + return GetStream (uid, string.Empty, cancellationToken, progress); + } /// - /// Asynchronously fetch the message summaries for the specified message indexes that - /// have a higher mod-sequence value than the one specified. + /// Asynchronously get a message stream. /// /// - /// Asynchronously fetches the message summaries for the specified message - /// indexes that have a higher mod-sequence value than the one specified. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. + /// Asynchronously gets a message stream. /// - /// An enumeration of summaries for the requested messages. - /// The indexes. - /// The mod-sequence value. - /// The message summary items to fetch. - /// The desired header fields. + /// + /// + /// + /// The message stream. + /// The UID of the message. /// The cancellation token. - /// - /// is null. - /// -or- - /// is null. - /// + /// The progress reporting mechanism. /// - /// One or more of the is invalid. - /// -or- - /// is empty. + /// is invalid. /// /// /// The has been disposed. @@ -6281,6 +5598,9 @@ public int Count { /// /// The folder is not currently open. /// + /// + /// The did not return the requested message stream. + /// /// /// The operation was canceled via the cancellation token. /// @@ -6293,48 +5613,26 @@ public int Count { /// /// The command failed. /// - public virtual Task> FetchAsync (IList indexes, ulong modseq, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)) + public virtual Task GetStreamAsync (UniqueId uid, CancellationToken cancellationToken = default, ITransferProgress? progress = null) { - if (indexes == null) - throw new ArgumentNullException (nameof (indexes)); - - if (fields == null) - throw new ArgumentNullException (nameof (fields)); - - if (fields.Count == 0) - throw new ArgumentException ("The set of header fields cannot be empty.", nameof (fields)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return Fetch (indexes, modseq, items, fields, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); + return GetStreamAsync (uid, string.Empty, cancellationToken, progress); } /// - /// Fetch the message summaries for the messages between the two indexes, inclusive. + /// Get a message stream. /// /// - /// Fetches the message summaries for the messages between the two - /// indexes, inclusive. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. + /// Gets a message stream. /// - /// An enumeration of summaries for the requested messages. - /// The minimum index. - /// The maximum index, or -1 to specify no upper bound. - /// The message summary items to fetch. + /// + /// + /// + /// The message stream. + /// The index of the message. /// The cancellation token. + /// The progress reporting mechanism. /// - /// is out of range. - /// -or- - /// is out of range. - /// -or- - /// is empty. + /// is out of range. /// /// /// The has been disposed. @@ -6348,6 +5646,9 @@ public int Count { /// /// The folder is not currently open. /// + /// + /// The did not return the requested message stream. + /// /// /// The operation was canceled via the cancellation token. /// @@ -6360,6176 +5661,26 @@ public int Count { /// /// The command failed. /// - public abstract IList Fetch (int min, int max, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)); + public virtual Stream GetStream (int index, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + return GetStream (index, string.Empty, cancellationToken, progress); + } /// - /// Asynchronously fetch the message summaries for the messages between the two indexes, inclusive. + /// Asynchronously get a message stream. /// /// - /// Asynchronously fetches the message summaries for the messages between - /// the two indexes, inclusive. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. + /// Asynchronously gets a message stream. /// - /// An enumeration of summaries for the requested messages. - /// The minimum index. - /// The maximum index, or -1 to specify no upper bound. - /// The message summary items to fetch. - /// The cancellation token. - /// - /// is out of range. - /// -or- - /// is out of range. - /// -or- - /// is empty. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public virtual Task> FetchAsync (int min, int max, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)) - { - if (min < 0 || min > Count) - throw new ArgumentOutOfRangeException (nameof (min)); - - if (max != -1 && max < min) - throw new ArgumentOutOfRangeException (nameof (max)); - - if (items == MessageSummaryItems.None) - throw new ArgumentOutOfRangeException (nameof (items)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return Fetch (min, max, items, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Fetch the message summaries for the messages between the two indexes, inclusive. - /// - /// - /// Fetches the message summaries for the messages between the two - /// indexes, inclusive. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. - /// - /// An enumeration of summaries for the requested messages. - /// The minimum index. - /// The maximum index, or -1 to specify no upper bound. - /// The message summary items to fetch. - /// The desired header fields. - /// The cancellation token. - /// - /// is out of range. - /// -or- - /// is out of range. - /// - /// - /// is null. - /// - /// - /// is empty. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public abstract IList Fetch (int min, int max, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously fetch the message summaries for the messages between the two indexes, inclusive. - /// - /// - /// Asynchronously fetches the message summaries for the messages between - /// the two indexes, inclusive. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. - /// - /// An enumeration of summaries for the requested messages. - /// The minimum index. - /// The maximum index, or -1 to specify no upper bound. - /// The message summary items to fetch. - /// The desired header fields. - /// The cancellation token. - /// - /// is out of range. - /// -or- - /// is out of range. - /// - /// - /// is null. - /// - /// - /// is empty. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public virtual Task> FetchAsync (int min, int max, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)) - { - if (min < 0 || min > Count) - throw new ArgumentOutOfRangeException (nameof (min)); - - if (max != -1 && max < min) - throw new ArgumentOutOfRangeException (nameof (max)); - - if (fields == null) - throw new ArgumentNullException (nameof (fields)); - - if (fields.Count == 0) - throw new ArgumentException ("The set of header fields cannot be empty.", nameof (fields)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return Fetch (min, max, items, fields, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Fetch the message summaries for the messages between the two indexes, inclusive. - /// - /// - /// Fetches the message summaries for the messages between the two - /// indexes, inclusive. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. - /// - /// An enumeration of summaries for the requested messages. - /// The minimum index. - /// The maximum index, or -1 to specify no upper bound. - /// The message summary items to fetch. - /// The desired header fields. - /// The cancellation token. - /// - /// is out of range. - /// -or- - /// is out of range. - /// - /// - /// is null. - /// - /// - /// is empty. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public abstract IList Fetch (int min, int max, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously fetch the message summaries for the messages between the two indexes, inclusive. - /// - /// - /// Asynchronously fetches the message summaries for the messages between - /// the two indexes, inclusive. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. - /// - /// An enumeration of summaries for the requested messages. - /// The minimum index. - /// The maximum index, or -1 to specify no upper bound. - /// The message summary items to fetch. - /// The desired header fields. + /// + /// + /// + /// The message stream. + /// The index of the message. /// The cancellation token. + /// The progress reporting mechanism. /// - /// is out of range. - /// -or- - /// is out of range. - /// - /// - /// is null. - /// - /// - /// is empty. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public virtual Task> FetchAsync (int min, int max, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)) - { - if (min < 0 || min > Count) - throw new ArgumentOutOfRangeException (nameof (min)); - - if (max != -1 && max < min) - throw new ArgumentOutOfRangeException (nameof (max)); - - if (fields == null) - throw new ArgumentNullException (nameof (fields)); - - if (fields.Count == 0) - throw new ArgumentException ("The set of header fields cannot be empty.", nameof (fields)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return Fetch (min, max, items, fields, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Fetch the message summaries for the messages between the two indexes (inclusive) - /// that have a higher mod-sequence value than the one specified. - /// - /// - /// Fetches the message summaries for the messages between the two - /// indexes (inclusive) that have a higher mod-sequence value than the one - /// specified. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. - /// - /// An enumeration of summaries for the requested messages. - /// The minimum index. - /// The maximum index, or -1 to specify no upper bound. - /// The mod-sequence value. - /// The message summary items to fetch. - /// The cancellation token. - /// - /// is out of range. - /// -or- - /// is out of range. - /// -or- - /// is empty. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open. - /// - /// - /// The does not support mod-sequences. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public abstract IList Fetch (int min, int max, ulong modseq, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously fetch the message summaries for the messages between the two indexes - /// (inclusive) that have a higher mod-sequence value than the one specified. - /// - /// - /// Asynchronously fetches the message summaries for the messages between - /// the two indexes (inclusive) that have a higher mod-sequence value than the - /// one specified. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. - /// - /// An enumeration of summaries for the requested messages. - /// The minimum index. - /// The maximum index, or -1 to specify no upper bound. - /// The mod-sequence value. - /// The message summary items to fetch. - /// The cancellation token. - /// - /// is out of range. - /// -or- - /// is out of range. - /// -or- - /// is empty. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open. - /// - /// - /// The does not support mod-sequences. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public virtual Task> FetchAsync (int min, int max, ulong modseq, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)) - { - if (min < 0) - throw new ArgumentOutOfRangeException (nameof (min)); - - if (max != -1 && max < min) - throw new ArgumentOutOfRangeException (nameof (max)); - - if (items == MessageSummaryItems.None) - throw new ArgumentOutOfRangeException (nameof (items)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return Fetch (min, max, modseq, items, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Fetch the message summaries for the messages between the two indexes (inclusive) - /// that have a higher mod-sequence value than the one specified. - /// - /// - /// Fetches the message summaries for the messages between the two - /// indexes (inclusive) that have a higher mod-sequence value than the one - /// specified. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. - /// - /// An enumeration of summaries for the requested messages. - /// The minimum index. - /// The maximum index, or -1 to specify no upper bound. - /// The mod-sequence value. - /// The message summary items to fetch. - /// The desired header fields. - /// The cancellation token. - /// - /// is out of range. - /// -or- - /// is out of range. - /// - /// - /// is null. - /// - /// - /// is empty. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open. - /// - /// - /// The does not support mod-sequences. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public abstract IList Fetch (int min, int max, ulong modseq, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously fetch the message summaries for the messages between the two indexes - /// (inclusive) that have a higher mod-sequence value than the one specified. - /// - /// - /// Asynchronously fetches the message summaries for the messages between - /// the two indexes (inclusive) that have a higher mod-sequence value than the - /// one specified. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. - /// - /// An enumeration of summaries for the requested messages. - /// The minimum index. - /// The maximum index, or -1 to specify no upper bound. - /// The mod-sequence value. - /// The message summary items to fetch. - /// The desired header fields. - /// The cancellation token. - /// - /// is out of range. - /// -or- - /// is out of range. - /// - /// - /// is null. - /// - /// - /// is empty. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open. - /// - /// - /// The does not support mod-sequences. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public virtual Task> FetchAsync (int min, int max, ulong modseq, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)) - { - if (min < 0) - throw new ArgumentOutOfRangeException (nameof (min)); - - if (max != -1 && max < min) - throw new ArgumentOutOfRangeException (nameof (max)); - - if (fields == null) - throw new ArgumentNullException (nameof (fields)); - - if (fields.Count == 0) - throw new ArgumentException ("The set of header fields cannot be empty.", nameof (fields)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return Fetch (min, max, modseq, items, fields, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Fetch the message summaries for the messages between the two indexes (inclusive) - /// that have a higher mod-sequence value than the one specified. - /// - /// - /// Fetches the message summaries for the messages between the two - /// indexes (inclusive) that have a higher mod-sequence value than the one - /// specified. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. - /// - /// An enumeration of summaries for the requested messages. - /// The minimum index. - /// The maximum index, or -1 to specify no upper bound. - /// The mod-sequence value. - /// The message summary items to fetch. - /// The desired header fields. - /// The cancellation token. - /// - /// is out of range. - /// -or- - /// is out of range. - /// - /// - /// is null. - /// - /// - /// is empty. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open. - /// - /// - /// The does not support mod-sequences. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public abstract IList Fetch (int min, int max, ulong modseq, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously fetch the message summaries for the messages between the two indexes - /// (inclusive) that have a higher mod-sequence value than the one specified. - /// - /// - /// Asynchronously fetches the message summaries for the messages between - /// the two indexes (inclusive) that have a higher mod-sequence value than the - /// one specified. - /// It should be noted that if another client has modified any message - /// in the folder, the mail service may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. - /// - /// An enumeration of summaries for the requested messages. - /// The minimum index. - /// The maximum index, or -1 to specify no upper bound. - /// The mod-sequence value. - /// The message summary items to fetch. - /// The desired header fields. - /// The cancellation token. - /// - /// is out of range. - /// -or- - /// is out of range. - /// - /// - /// is null. - /// - /// - /// is empty. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open. - /// - /// - /// The does not support mod-sequences. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public virtual Task> FetchAsync (int min, int max, ulong modseq, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)) - { - if (min < 0) - throw new ArgumentOutOfRangeException (nameof (min)); - - if (max != -1 && max < min) - throw new ArgumentOutOfRangeException (nameof (max)); - - if (fields == null) - throw new ArgumentNullException (nameof (fields)); - - if (fields.Count == 0) - throw new ArgumentException ("The set of header fields cannot be empty.", nameof (fields)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return Fetch (min, max, modseq, items, fields, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Get the specified message headers. - /// - /// - /// Gets the specified message headers. - /// - /// The message headers. - /// The UID of the message. - /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is invalid. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open. - /// - /// - /// The did not return the requested message headers. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public abstract HeaderList GetHeaders (UniqueId uid, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Asynchronously get the specified message headers. - /// - /// - /// Asynchronously gets the specified message headers. - /// - /// The message headers. - /// The UID of the message. - /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is invalid. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open. - /// - /// - /// The did not return the requested message headers. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public virtual Task GetHeadersAsync (UniqueId uid, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) - { - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return GetHeaders (uid, cancellationToken, progress); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Get the specified body part headers. - /// - /// - /// Gets the specified body part headers. - /// - /// The body part headers. - /// The UID of the message. - /// The body part. - /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is null. - /// - /// - /// is invalid. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open. - /// - /// - /// The did not return the requested body part headers. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public abstract HeaderList GetHeaders (UniqueId uid, BodyPart part, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Asynchronously get the specified body part headers. - /// - /// - /// Asynchronously gets the specified body part headers. - /// - /// The body part headers. - /// The UID of the message. - /// The body part. - /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is null. - /// - /// - /// is invalid. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open. - /// - /// - /// The did not return the requested body part headers. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public virtual Task GetHeadersAsync (UniqueId uid, BodyPart part, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) - { - if (part == null) - throw new ArgumentNullException (nameof (part)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return GetHeaders (uid, part, cancellationToken, progress); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Get the specified message headers. - /// - /// - /// Gets the specified message headers. - /// - /// The message headers. - /// The index of the message. - /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is out of range. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open. - /// - /// - /// The did not return the requested message headers. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public abstract HeaderList GetHeaders (int index, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Asynchronously get the specified message headers. - /// - /// - /// Asynchronously gets the specified message headers. - /// - /// The message headers. - /// The index of the message. - /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is out of range. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open. - /// - /// - /// The did not return the requested message headers. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public virtual Task GetHeadersAsync (int index, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) - { - if (index < 0 || index >= Count) - throw new ArgumentOutOfRangeException (nameof (index)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return GetHeaders (index, cancellationToken, progress); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Get the specified body part headers. - /// - /// - /// Gets the specified body part headers. - /// - /// The body part headers. - /// The index of the message. - /// The body part. - /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is out of range. - /// - /// - /// is null. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open. - /// - /// - /// The did not return the requested body part headers. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public abstract HeaderList GetHeaders (int index, BodyPart part, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Asynchronously get the specified body part headers. - /// - /// - /// Asynchronously gets the specified body part headers. - /// - /// The body part headers. - /// The index of the message. - /// The body part. - /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is out of range. - /// - /// - /// is null. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open. - /// - /// - /// The did not return the requested body part headers. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public virtual Task GetHeadersAsync (int index, BodyPart part, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) - { - if (index < 0 || index >= Count) - throw new ArgumentOutOfRangeException (nameof (index)); - - if (part == null) - throw new ArgumentNullException (nameof (part)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return GetHeaders (index, part, cancellationToken, progress); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Get the specified message. - /// - /// - /// Gets the specified message. - /// - /// The message. - /// The UID of the message. - /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is invalid. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open. - /// - /// - /// The did not return the requested message. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public abstract MimeMessage GetMessage (UniqueId uid, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Asynchronously get the specified message. - /// - /// - /// Asynchronously gets the specified message. - /// - /// The message. - /// The UID of the message. - /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is invalid. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open. - /// - /// - /// The did not return the requested message. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public virtual Task GetMessageAsync (UniqueId uid, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) - { - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return GetMessage (uid, cancellationToken, progress); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Get the specified message. - /// - /// - /// Gets the specified message. - /// - /// The message. - /// The index of the message. - /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is out of range. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open. - /// - /// - /// The did not return the requested message. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public abstract MimeMessage GetMessage (int index, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Asynchronously get the specified message. - /// - /// - /// Asynchronously gets the specified message. - /// - /// The message. - /// The index of the message. - /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is out of range. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open. - /// - /// - /// The did not return the requested message. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public virtual Task GetMessageAsync (int index, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) - { - if (index < 0 || index >= Count) - throw new ArgumentOutOfRangeException (nameof (index)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return GetMessage (index, cancellationToken, progress); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Get the specified body part. - /// - /// - /// Gets the specified body part. - /// - /// - /// - /// - /// The body part. - /// The UID of the message. - /// The body part. - /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is null. - /// - /// - /// is invalid. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open. - /// - /// - /// The did not return the requested message body. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public abstract MimeEntity GetBodyPart (UniqueId uid, BodyPart part, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Asynchronously get the specified body part. - /// - /// - /// Asynchronously gets the specified body part. - /// - /// The body part. - /// The UID of the message. - /// The body part. - /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is null. - /// - /// - /// is invalid. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open. - /// - /// - /// The did not return the requested message body. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public virtual Task GetBodyPartAsync (UniqueId uid, BodyPart part, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) - { - if (part == null) - throw new ArgumentNullException (nameof (part)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return GetBodyPart (uid, part, cancellationToken, progress); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Get the specified body part. - /// - /// - /// Gets the specified body part. - /// - /// The body part. - /// The UID of the message. - /// The body part. - /// true if only the headers should be downloaded; otherwise, false> - /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is null. - /// - /// - /// is invalid. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open. - /// - /// - /// The did not return the requested message body. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - [Obsolete ("Use GetBodyPart(UniqueId, BodyPart, CancellationToken, ITransferProgress) or GetHeaders(UniqueId, BodyPart, CancellationToken, ITransferProgress)")] - public abstract MimeEntity GetBodyPart (UniqueId uid, BodyPart part, bool headersOnly, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Asynchronously get the specified body part. - /// - /// - /// Asynchronously gets the specified body part. - /// - /// The body part. - /// The UID of the message. - /// The body part. - /// true if only the headers should be downloaded; otherwise, false> - /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is null. - /// - /// - /// is invalid. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open. - /// - /// - /// The did not return the requested message body. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - [Obsolete ("Use GetBodyPartAsync(UniqueId, BodyPart, CancellationToken, ITransferProgress) or GetHeadersAsync(UniqueId, BodyPart, CancellationToken, ITransferProgress)")] - public virtual Task GetBodyPartAsync (UniqueId uid, BodyPart part, bool headersOnly, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) - { - if (part == null) - throw new ArgumentNullException (nameof (part)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return GetBodyPart (uid, part, headersOnly, cancellationToken, progress); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Get the specified body part. - /// - /// - /// Gets the specified body part. - /// - /// The body part. - /// The index of the message. - /// The body part. - /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is null. - /// - /// - /// is out of range. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open. - /// - /// - /// The did not return the requested message body. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public abstract MimeEntity GetBodyPart (int index, BodyPart part, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Asynchronously get the specified body part. - /// - /// - /// Asynchronously gets the specified body part. - /// - /// The body part. - /// The index of the message. - /// The body part. - /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is null. - /// - /// - /// is out of range. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open. - /// - /// - /// The did not return the requested message body. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public virtual Task GetBodyPartAsync (int index, BodyPart part, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) - { - if (index < 0 || index >= Count) - throw new ArgumentOutOfRangeException (nameof (index)); - - if (part == null) - throw new ArgumentNullException (nameof (part)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return GetBodyPart (index, part, cancellationToken, progress); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Get the specified body part. - /// - /// - /// Gets the specified body part. - /// - /// The body part. - /// The index of the message. - /// The body part. - /// true if only the headers should be downloaded; otherwise, false> - /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is null. - /// - /// - /// is out of range. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open. - /// - /// - /// The did not return the requested message body. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - [Obsolete ("Use GetBodyPart(int, BodyPart, CancellationToken, ITransferProgress) or GetHeaders(int, BodyPart, CancellationToken, ITransferProgress)")] - public abstract MimeEntity GetBodyPart (int index, BodyPart part, bool headersOnly, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Asynchronously get the specified body part. - /// - /// - /// Asynchronously gets the specified body part. - /// - /// The body part. - /// The index of the message. - /// The body part. - /// true if only the headers should be downloaded; otherwise, false> - /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is null. - /// - /// - /// is out of range. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open. - /// - /// - /// The did not return the requested message body. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - [Obsolete ("Use GetBodyPartAsync(int, BodyPart, CancellationToken, ITransferProgress) or GetHeadersAsync(int, BodyPart, CancellationToken, ITransferProgress)")] - public virtual Task GetBodyPartAsync (int index, BodyPart part, bool headersOnly, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) - { - if (index < 0 || index >= Count) - throw new ArgumentOutOfRangeException (nameof (index)); - - if (part == null) - throw new ArgumentNullException (nameof (part)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return GetBodyPart (index, part, headersOnly, cancellationToken, progress); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Get a substream of the specified message. - /// - /// - /// Gets a substream of the message. If the starting offset is beyond - /// the end of the message, an empty stream is returned. If the number of - /// bytes desired extends beyond the end of the message, a truncated stream - /// will be returned. - /// - /// The stream. - /// The UID of the message. - /// The starting offset of the first desired byte. - /// The number of bytes desired. - /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is invalid. - /// - /// - /// is negative. - /// -or- - /// is negative. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open. - /// - /// - /// The did not return the requested message stream. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public abstract Stream GetStream (UniqueId uid, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Asynchronously get a substream of the specified message. - /// - /// - /// Asynchronously gets a substream of the message. If the starting offset is beyond - /// the end of the message, an empty stream is returned. If the number of - /// bytes desired extends beyond the end of the message, a truncated stream - /// will be returned. - /// - /// The stream. - /// The UID of the message. - /// The starting offset of the first desired byte. - /// The number of bytes desired. - /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is invalid. - /// - /// - /// is negative. - /// -or- - /// is negative. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open. - /// - /// - /// The did not return the requested message stream. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public virtual Task GetStreamAsync (UniqueId uid, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) - { - if (offset < 0) - throw new ArgumentOutOfRangeException (nameof (offset)); - - if (count < 0) - throw new ArgumentOutOfRangeException (nameof (count)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return GetStream (uid, offset, count, cancellationToken, progress); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Get a substream of the specified message. - /// - /// - /// Gets a substream of the message. If the starting offset is beyond - /// the end of the message, an empty stream is returned. If the number of - /// bytes desired extends beyond the end of the message, a truncated stream - /// will be returned. - /// - /// The stream. - /// The index of the message. - /// The starting offset of the first desired byte. - /// The number of bytes desired. - /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is out of range. - /// -or- - /// is negative. - /// -or- - /// is negative. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open. - /// - /// - /// The did not return the requested message stream. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public abstract Stream GetStream (int index, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Asynchronously get a substream of the specified message. - /// - /// - /// Asynchronously gets a substream of the message. If the starting offset is beyond - /// the end of the message, an empty stream is returned. If the number of - /// bytes desired extends beyond the end of the message, a truncated stream - /// will be returned. - /// - /// The stream. - /// The index of the message. - /// The starting offset of the first desired byte. - /// The number of bytes desired. - /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is out of range. - /// -or- - /// is negative. - /// -or- - /// is negative. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open. - /// - /// - /// The did not return the requested message stream. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public virtual Task GetStreamAsync (int index, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) - { - if (index < 0 || index >= Count) - throw new ArgumentOutOfRangeException (nameof (index)); - - if (offset < 0) - throw new ArgumentOutOfRangeException (nameof (offset)); - - if (count < 0) - throw new ArgumentOutOfRangeException (nameof (count)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return GetStream (index, offset, count, cancellationToken, progress); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Get a substream of the specified body part. - /// - /// - /// Gets a substream of the body part. If the starting offset is beyond - /// the end of the body part, an empty stream is returned. If the number of - /// bytes desired extends beyond the end of the body part, a truncated stream - /// will be returned. - /// - /// The stream. - /// The UID of the message. - /// The desired body part. - /// The starting offset of the first desired byte. - /// The number of bytes desired. - /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is invalid. - /// - /// - /// is null. - /// - /// - /// is negative. - /// -or- - /// is negative. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open. - /// - /// - /// The did not return the requested message stream. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public virtual Stream GetStream (UniqueId uid, BodyPart part, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) - { - if (uid.Id == 0) - throw new ArgumentException ("The uid is invalid.", nameof (uid)); - - if (part == null) - throw new ArgumentNullException (nameof (part)); - - if (offset < 0) - throw new ArgumentOutOfRangeException (nameof (offset)); - - if (count < 0) - throw new ArgumentOutOfRangeException (nameof (count)); - - return GetStream (uid, part.PartSpecifier, offset, count, cancellationToken, progress); - } - - /// - /// Asynchronously get a substream of the specified body part. - /// - /// - /// Asynchronously gets a substream of the body part. If the starting offset is beyond - /// the end of the body part, an empty stream is returned. If the number of - /// bytes desired extends beyond the end of the body part, a truncated stream - /// will be returned. - /// - /// The stream. - /// The UID of the message. - /// The desired body part. - /// The starting offset of the first desired byte. - /// The number of bytes desired. - /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is invalid. - /// - /// - /// is null. - /// - /// - /// is negative. - /// -or- - /// is negative. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open. - /// - /// - /// The did not return the requested message stream. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public virtual Task GetStreamAsync (UniqueId uid, BodyPart part, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) - { - if (part == null) - throw new ArgumentNullException (nameof (part)); - - if (offset < 0) - throw new ArgumentOutOfRangeException (nameof (offset)); - - if (count < 0) - throw new ArgumentOutOfRangeException (nameof (count)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return GetStream (uid, part, offset, count, cancellationToken, progress); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Get a substream of the specified body part. - /// - /// - /// Gets a substream of the body part. If the starting offset is beyond - /// the end of the body part, an empty stream is returned. If the number of - /// bytes desired extends beyond the end of the body part, a truncated stream - /// will be returned. - /// - /// The stream. - /// The index of the message. - /// The desired body part. - /// The starting offset of the first desired byte. - /// The number of bytes desired. - /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is null. - /// - /// - /// is out of range. - /// -or- - /// is negative. - /// -or- - /// is negative. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open. - /// - /// - /// The did not return the requested message stream. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public virtual Stream GetStream (int index, BodyPart part, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) - { - if (index < 0 || index >= Count) - throw new ArgumentOutOfRangeException (nameof (index)); - - if (part == null) - throw new ArgumentNullException (nameof (part)); - - if (offset < 0) - throw new ArgumentOutOfRangeException (nameof (offset)); - - if (count < 0) - throw new ArgumentOutOfRangeException (nameof (count)); - - return GetStream (index, part.PartSpecifier, offset, count, cancellationToken, progress); - } - - /// - /// Asynchronously get a substream of the specified body part. - /// - /// - /// Asynchronously gets a substream of the body part. If the starting offset is beyond - /// the end of the body part, an empty stream is returned. If the number of - /// bytes desired extends beyond the end of the body part, a truncated stream - /// will be returned. - /// - /// The stream. - /// The index of the message. - /// The desired body part. - /// The starting offset of the first desired byte. - /// The number of bytes desired. - /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is null. - /// - /// - /// is out of range. - /// -or- - /// is negative. - /// -or- - /// is negative. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open. - /// - /// - /// The did not return the requested message stream. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public virtual Task GetStreamAsync (int index, BodyPart part, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) - { - if (index < 0 || index >= Count) - throw new ArgumentOutOfRangeException (nameof (index)); - - if (part == null) - throw new ArgumentNullException (nameof (part)); - - if (offset < 0) - throw new ArgumentOutOfRangeException (nameof (offset)); - - if (count < 0) - throw new ArgumentOutOfRangeException (nameof (count)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return GetStream (index, part, offset, count, cancellationToken, progress); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Get a substream of the specified message. - /// - /// - /// Gets a substream of the specified message. - /// For more information about how to construct the , - /// see Section 6.4.5 of RFC3501. - /// - /// The stream. - /// The UID of the message. - /// The desired section of the message. - /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is invalid. - /// - /// - /// is null. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open. - /// - /// - /// The did not return the requested message stream. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public abstract Stream GetStream (UniqueId uid, string section, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Asynchronously get a substream of the specified message. - /// - /// - /// Asynchronously gets a substream of the specified message. - /// For more information about how to construct the , - /// see Section 6.4.5 of RFC3501. - /// - /// The stream. - /// The UID of the message. - /// The desired section of the message. - /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is invalid. - /// - /// - /// is null. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open. - /// - /// - /// The did not return the requested message stream. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public virtual Task GetStreamAsync (UniqueId uid, string section, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) - { - if (section == null) - throw new ArgumentNullException (nameof (section)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return GetStream (uid, section, cancellationToken, progress); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Get a substream of the specified message. - /// - /// - /// Gets a substream of the specified message. If the starting offset is beyond - /// the end of the specified section of the message, an empty stream is returned. If - /// the number of bytes desired extends beyond the end of the section, a truncated - /// stream will be returned. - /// For more information about how to construct the , - /// see Section 6.4.5 of RFC3501. - /// - /// The stream. - /// The UID of the message. - /// The desired section of the message. - /// The starting offset of the first desired byte. - /// The number of bytes desired. - /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is invalid. - /// - /// - /// is null. - /// - /// - /// is negative. - /// -or- - /// is negative. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open. - /// - /// - /// The did not return the requested message stream. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public abstract Stream GetStream (UniqueId uid, string section, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Asynchronously get a substream of the specified message. - /// - /// - /// Asynchronously gets a substream of the specified message. If the starting - /// offset is beyond the end of the specified section of the message, an empty stream - /// is returned. If the number of bytes desired extends beyond the end of the section, - /// a truncated stream will be returned. - /// For more information about how to construct the , - /// see Section 6.4.5 of RFC3501. - /// - /// The stream. - /// The UID of the message. - /// The desired section of the message. - /// The starting offset of the first desired byte. - /// The number of bytes desired. - /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is invalid. - /// - /// - /// is null. - /// - /// - /// is negative. - /// -or- - /// is negative. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open. - /// - /// - /// The did not return the requested message stream. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public virtual Task GetStreamAsync (UniqueId uid, string section, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) - { - if (section == null) - throw new ArgumentNullException (nameof (section)); - - if (offset < 0) - throw new ArgumentOutOfRangeException (nameof (offset)); - - if (count < 0) - throw new ArgumentOutOfRangeException (nameof (count)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return GetStream (uid, section, offset, count, cancellationToken, progress); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Get a substream of the specified message. - /// - /// - /// Gets a substream of the specified message. - /// For more information about how to construct the , - /// see Section 6.4.5 of RFC3501. - /// - /// The stream. - /// The index of the message. - /// The desired section of the message. - /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is null. - /// - /// - /// is out of range. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open. - /// - /// - /// The did not return the requested message stream. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public abstract Stream GetStream (int index, string section, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Asynchronously get a substream of the specified body part. - /// - /// - /// Asynchronously gets a substream of the specified message. - /// For more information about how to construct the , - /// see Section 6.4.5 of RFC3501. - /// - /// The stream. - /// The index of the message. - /// The desired section of the message. - /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is null. - /// - /// - /// is out of range. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open. - /// - /// - /// The did not return the requested message stream. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public virtual Task GetStreamAsync (int index, string section, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) - { - if (index < 0 || index >= Count) - throw new ArgumentOutOfRangeException (nameof (index)); - - if (section == null) - throw new ArgumentNullException (nameof (section)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return GetStream (index, section, cancellationToken, progress); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Get a substream of the specified message. - /// - /// - /// Gets a substream of the specified message. If the starting offset is beyond - /// the end of the specified section of the message, an empty stream is returned. If - /// the number of bytes desired extends beyond the end of the section, a truncated - /// stream will be returned. - /// For more information about how to construct the , - /// see Section 6.4.5 of RFC3501. - /// - /// The stream. - /// The index of the message. - /// The desired section of the message. - /// The starting offset of the first desired byte. - /// The number of bytes desired. - /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is null. - /// - /// - /// is out of range. - /// -or- - /// is negative. - /// -or- - /// is negative. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open. - /// - /// - /// The did not return the requested message stream. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public abstract Stream GetStream (int index, string section, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); - - /// - /// Asynchronously get a substream of the specified body part. - /// - /// - /// Asynchronously gets a substream of the specified message. If the starting - /// offset is beyond the end of the specified section of the message, an empty stream - /// is returned. If the number of bytes desired extends beyond the end of the section, - /// a truncated stream will be returned. - /// For more information about how to construct the , - /// see Section 6.4.5 of RFC3501. - /// - /// The stream. - /// The index of the message. - /// The desired section of the message. - /// The starting offset of the first desired byte. - /// The number of bytes desired. - /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is null. - /// - /// - /// is out of range. - /// -or- - /// is negative. - /// -or- - /// is negative. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open. - /// - /// - /// The did not return the requested message stream. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public virtual Task GetStreamAsync (int index, string section, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) - { - if (index < 0 || index >= Count) - throw new ArgumentOutOfRangeException (nameof (index)); - - if (section == null) - throw new ArgumentNullException (nameof (section)); - - if (offset < 0) - throw new ArgumentOutOfRangeException (nameof (offset)); - - if (count < 0) - throw new ArgumentOutOfRangeException (nameof (count)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return GetStream (index, section, offset, count, cancellationToken, progress); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Add a set of flags to the specified message. - /// - /// - /// Adds a set of flags to the specified message. - /// - /// The UID of the message. - /// The message flags to add. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is invalid. - /// -or- - /// No flags were specified. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public void AddFlags (UniqueId uid, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - AddFlags (new [] { uid }, flags, silent, cancellationToken); - } - - /// - /// Asynchronously add a set of flags to the specified message. - /// - /// - /// Asynchronously adds a set of flags to the specified message. - /// - /// An asynchronous task context. - /// The UID of the message. - /// The message flags to add. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is invalid. - /// -or- - /// No flags were specified. - /// - /// - /// The has been disposed. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public Task AddFlagsAsync (UniqueId uid, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - return AddFlagsAsync (new [] { uid }, flags, silent, cancellationToken); - } - - /// - /// Add a set of flags to the specified message. - /// - /// - /// Adds a set of flags to the specified message. - /// - /// The UID of the message. - /// The message flags to add. - /// A set of user-defined flags to add. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is invalid. - /// -or- - /// No flags were specified. - /// - /// - /// The has been disposed. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public void AddFlags (UniqueId uid, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - AddFlags (new [] { uid }, flags, userFlags, silent, cancellationToken); - } - - /// - /// Asynchronously add a set of flags to the specified message. - /// - /// - /// Asynchronously adds a set of flags to the specified message. - /// - /// An asynchronous task context. - /// The UID of the message. - /// The message flags to add. - /// A set of user-defined flags to add. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is invalid. - /// -or- - /// No flags were specified. - /// - /// - /// The has been disposed. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public Task AddFlagsAsync (UniqueId uid, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - return AddFlagsAsync (new [] { uid }, flags, userFlags, silent, cancellationToken); - } - - /// - /// Add a set of flags to the specified messages. - /// - /// - /// Adds a set of flags to the specified messages. - /// - /// The UIDs of the messages. - /// The message flags to add. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// No flags were specified. - /// - /// - /// The has been disposed. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public virtual void AddFlags (IList uids, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - AddFlags (uids, flags, null, silent, cancellationToken); - } - - /// - /// Asynchronously add a set of flags to the specified messages. - /// - /// - /// Asynchronously adds a set of flags to the specified messages. - /// - /// An asynchronous task context. - /// The UIDs of the messages. - /// The message flags to add. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// No flags were specified. - /// - /// - /// The has been disposed. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public virtual Task AddFlagsAsync (IList uids, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - if (uids == null) - throw new ArgumentNullException (nameof (uids)); - - if ((flags & SettableFlags) == 0) - throw new ArgumentException ("No flags were specified.", nameof (flags)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - AddFlags (uids, flags, silent, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Add a set of flags to the specified messages. - /// - /// - /// Adds a set of flags to the specified messages. - /// - /// The UIDs of the messages. - /// The message flags to add. - /// A set of user-defined flags to add. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// No flags were specified. - /// - /// - /// The has been disposed. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public abstract void AddFlags (IList uids, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously add a set of flags to the specified messages. - /// - /// - /// Asynchronously adds a set of flags to the specified messages. - /// - /// An asynchronous task context. - /// The UIDs of the messages. - /// The message flags to add. - /// A set of user-defined flags to add. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// No flags were specified. - /// - /// - /// The has been disposed. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public virtual Task AddFlagsAsync (IList uids, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - if (uids == null) - throw new ArgumentNullException (nameof (uids)); - - if ((flags & SettableFlags) == 0 && (userFlags == null || userFlags.Count == 0)) - throw new ArgumentException ("No flags were specified.", nameof (flags)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - AddFlags (uids, flags, userFlags, silent, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Remove a set of flags from the specified message. - /// - /// - /// Removes a set of flags from the specified message. - /// - /// The UIDs of the message. - /// The message flags to remove. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is invalid. - /// -or- - /// No flags were specified. - /// - /// - /// The has been disposed. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public void RemoveFlags (UniqueId uid, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - RemoveFlags (new [] { uid }, flags, silent, cancellationToken); - } - - /// - /// Asynchronously remove a set of flags from the specified message. - /// - /// - /// Asynchronously removes a set of flags from the specified message. - /// - /// An asynchronous task context. - /// The UID of the message. - /// The message flags to remove. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is invalid. - /// -or- - /// No flags were specified. - /// - /// - /// The has been disposed. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public Task RemoveFlagsAsync (UniqueId uid, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - return RemoveFlagsAsync (new [] { uid }, flags, silent, cancellationToken); - } - - /// - /// Remove a set of flags from the specified message. - /// - /// - /// Removes a set of flags from the specified message. - /// - /// The UIDs of the message. - /// The message flags to remove. - /// A set of user-defined flags to remove. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is invalid. - /// -or- - /// No flags were specified. - /// - /// - /// The has been disposed. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public void RemoveFlags (UniqueId uid, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - RemoveFlags (new [] { uid }, flags, userFlags, silent, cancellationToken); - } - - /// - /// Asynchronously remove a set of flags from the specified message. - /// - /// - /// Asynchronously removes a set of flags from the specified message. - /// - /// An asynchronous task context. - /// The UID of the message. - /// The message flags to remove. - /// A set of user-defined flags to remove. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is invalid. - /// -or- - /// No flags were specified. - /// - /// - /// The has been disposed. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public Task RemoveFlagsAsync (UniqueId uid, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - return RemoveFlagsAsync (new [] { uid }, flags, userFlags, silent, cancellationToken); - } - - /// - /// Remove a set of flags from the specified messages. - /// - /// - /// Removes a set of flags from the specified messages. - /// - /// The UIDs of the messages. - /// The message flags to remove. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// No flags were specified. - /// - /// - /// The has been disposed. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public virtual void RemoveFlags (IList uids, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - RemoveFlags (uids, flags, null, silent, cancellationToken); - } - - /// - /// Asynchronously remove a set of flags from the specified messages. - /// - /// - /// Asynchronously removes a set of flags from the specified messages. - /// - /// An asynchronous task context. - /// The UIDs of the messages. - /// The message flags to remove. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// No flags were specified. - /// - /// - /// The has been disposed. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public virtual Task RemoveFlagsAsync (IList uids, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - if (uids == null) - throw new ArgumentNullException (nameof (uids)); - - if ((flags & SettableFlags) == 0) - throw new ArgumentException ("No flags were specified.", nameof (flags)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - RemoveFlags (uids, flags, silent, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Remove a set of flags from the specified messages. - /// - /// - /// Removes a set of flags from the specified messages. - /// - /// The UIDs of the messages. - /// The message flags to remove. - /// A set of user-defined flags to remove. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// No flags were specified. - /// - /// - /// The has been disposed. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public abstract void RemoveFlags (IList uids, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously remove a set of flags from the specified messages. - /// - /// - /// Asynchronously removes a set of flags from the specified messages. - /// - /// An asynchronous task context. - /// The UIDs of the messages. - /// The message flags to remove. - /// A set of user-defined flags to remove. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// No flags were specified. - /// - /// - /// The has been disposed. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public virtual Task RemoveFlagsAsync (IList uids, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - if (uids == null) - throw new ArgumentNullException (nameof (uids)); - - if ((flags & SettableFlags) == 0 && (userFlags == null || userFlags.Count == 0)) - throw new ArgumentException ("No flags were specified.", nameof (flags)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - RemoveFlags (uids, flags, userFlags, silent, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Set the flags of the specified message. - /// - /// - /// Sets the flags of the specified message. - /// - /// The UIDs of the message. - /// The message flags to set. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is invalid. - /// - /// - /// The has been disposed. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public void SetFlags (UniqueId uid, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - SetFlags (new [] { uid }, flags, silent, cancellationToken); - } - - /// - /// Asynchronously set the flags of the specified message. - /// - /// - /// Asynchronously sets the flags of the specified message. - /// - /// An asynchronous task context. - /// The UID of the message. - /// The message flags to set. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is invalid. - /// - /// - /// The has been disposed. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public Task SetFlagsAsync (UniqueId uid, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - return SetFlagsAsync (new [] { uid }, flags, silent, cancellationToken); - } - - /// - /// Set the flags of the specified message. - /// - /// - /// Sets the flags of the specified message. - /// - /// The UIDs of the message. - /// The message flags to set. - /// A set of user-defined flags to set. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is invalid. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public void SetFlags (UniqueId uid, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - SetFlags (new [] { uid }, flags, userFlags, silent, cancellationToken); - } - - /// - /// Asynchronously set the flags of the specified message. - /// - /// - /// Asynchronously sets the flags of the specified message. - /// - /// An asynchronous task context. - /// The UID of the message. - /// The message flags to set. - /// A set of user-defined flags to set. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is invalid. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public Task SetFlagsAsync (UniqueId uid, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - return SetFlagsAsync (new [] { uid }, flags, userFlags, silent, cancellationToken); - } - - /// - /// Set the flags of the specified messages. - /// - /// - /// Sets the flags of the specified messages. - /// - /// The UIDs of the messages. - /// The message flags to set. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the is invalid. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public virtual void SetFlags (IList uids, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - SetFlags (uids, flags, null, silent, cancellationToken); - } - - /// - /// Asynchronously set the flags of the specified messages. - /// - /// - /// Asynchronously sets the flags of the specified messages. - /// - /// An asynchronous task context. - /// The UIDs of the messages. - /// The message flags to set. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the is invalid. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public virtual Task SetFlagsAsync (IList uids, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - if (uids == null) - throw new ArgumentNullException (nameof (uids)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - SetFlags (uids, flags, silent, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Set the flags of the specified messages. - /// - /// - /// Sets the flags of the specified messages. - /// - /// The UIDs of the messages. - /// The message flags to set. - /// A set of user-defined flags to set. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the is invalid. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public abstract void SetFlags (IList uids, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously set the flags of the specified messages. - /// - /// - /// Asynchronously sets the flags of the specified messages. - /// - /// An asynchronous task context. - /// The UIDs of the messages. - /// The message flags to set. - /// A set of user-defined flags to set. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the is invalid. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public virtual Task SetFlagsAsync (IList uids, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - if (uids == null) - throw new ArgumentNullException (nameof (uids)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - SetFlags (uids, flags, userFlags, silent, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Add a set of flags to the specified messages only if their mod-sequence value is less than the specified value. - /// - /// - /// Adds a set of flags to the specified messages only if their mod-sequence value is less than the specified value. - /// - /// The unique IDs of the messages that were not updated. - /// The UIDs of the messages. - /// The mod-sequence value. - /// The message flags to add. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// No flags were specified. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The does not support mod-sequences. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public virtual IList AddFlags (IList uids, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - return AddFlags (uids, modseq, flags, null, silent, cancellationToken); - } - - /// - /// Asynchronously add a set of flags to the specified messages only if their mod-sequence value is less than the specified value. - /// - /// - /// Asynchronously adds a set of flags to the specified messages only if their mod-sequence value is less than the specified value. - /// - /// The unique IDs of the messages that were not updated. - /// The UIDs of the messages. - /// The mod-sequence value. - /// The message flags to add. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// No flags were specified. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The does not support mod-sequences. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public virtual Task> AddFlagsAsync (IList uids, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - if (uids == null) - throw new ArgumentNullException (nameof (uids)); - - if ((flags & SettableFlags) == 0) - throw new ArgumentException ("No flags were specified.", nameof (flags)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return AddFlags (uids, modseq, flags, silent, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Add a set of flags to the specified messages only if their mod-sequence value is less than the specified value. - /// - /// - /// Adds a set of flags to the specified messages only if their mod-sequence value is less than the specified value. - /// - /// The unique IDs of the messages that were not updated. - /// The UIDs of the messages. - /// The mod-sequence value. - /// The message flags to add. - /// A set of user-defined flags to add. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// No flags were specified. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The does not support mod-sequences. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public abstract IList AddFlags (IList uids, ulong modseq, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously add a set of flags to the specified messages only if their mod-sequence value is less than the specified value. - /// - /// - /// Asynchronously adds a set of flags to the specified messages only if their mod-sequence value is less than the specified value. - /// - /// The unique IDs of the messages that were not updated. - /// The UIDs of the messages. - /// The mod-sequence value. - /// The message flags to add. - /// A set of user-defined flags to add. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// No flags were specified. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The does not support mod-sequences. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public virtual Task> AddFlagsAsync (IList uids, ulong modseq, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - if (uids == null) - throw new ArgumentNullException (nameof (uids)); - - if ((flags & SettableFlags) == 0 && (userFlags == null || userFlags.Count == 0)) - throw new ArgumentException ("No flags were specified.", nameof (flags)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return AddFlags (uids, modseq, flags, userFlags, silent, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Remove a set of flags from the specified messages only if their mod-sequence value is less than the specified value. - /// - /// - /// Removes a set of flags from the specified messages only if their mod-sequence value is less than the specified value. - /// - /// The unique IDs of the messages that were not updated. - /// The UIDs of the messages. - /// The mod-sequence value. - /// The message flags to remove. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// No flags were specified. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The does not support mod-sequences. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public virtual IList RemoveFlags (IList uids, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - return RemoveFlags (uids, modseq, flags, null, silent, cancellationToken); - } - - /// - /// Asynchronously remove a set of flags from the specified messages only if their mod-sequence value is less than the specified value. - /// - /// - /// Asynchronously removes a set of flags from the specified messages only if their mod-sequence value is less than the specified value. - /// - /// The unique IDs of the messages that were not updated. - /// The UIDs of the messages. - /// The mod-sequence value. - /// The message flags to remove. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// No flags were specified. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The does not support mod-sequences. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public virtual Task> RemoveFlagsAsync (IList uids, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - if (uids == null) - throw new ArgumentNullException (nameof (uids)); - - if ((flags & SettableFlags) == 0) - throw new ArgumentException ("No flags were specified.", nameof (flags)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return RemoveFlags (uids, modseq, flags, silent, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Remove a set of flags from the specified messages only if their mod-sequence value is less than the specified value. - /// - /// - /// Removes a set of flags from the specified messages only if their mod-sequence value is less than the specified value. - /// - /// The unique IDs of the messages that were not updated. - /// The UIDs of the messages. - /// The mod-sequence value. - /// The message flags to remove. - /// A set of user-defined flags to remove. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// No flags were specified. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The does not support mod-sequences. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public abstract IList RemoveFlags (IList uids, ulong modseq, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously remove a set of flags from the specified messages only if their mod-sequence value is less than the specified value. - /// - /// - /// Asynchronously removes a set of flags from the specified messages only if their mod-sequence value is less than the specified value. - /// - /// The unique IDs of the messages that were not updated. - /// The UIDs of the messages. - /// The mod-sequence value. - /// The message flags to remove. - /// A set of user-defined flags to remove. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// No flags were specified. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The does not support mod-sequences. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public virtual Task> RemoveFlagsAsync (IList uids, ulong modseq, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - if (uids == null) - throw new ArgumentNullException (nameof (uids)); - - if ((flags & SettableFlags) == 0 && (userFlags == null || userFlags.Count == 0)) - throw new ArgumentException ("No flags were specified.", nameof (flags)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return RemoveFlags (uids, modseq, flags, userFlags, silent, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Set the flags of the specified messages only if their mod-sequence value is less than the specified value. - /// - /// - /// Sets the flags of the specified messages only if their mod-sequence value is less than the specified value. - /// - /// The unique IDs of the messages that were not updated. - /// The UIDs of the messages. - /// The mod-sequence value. - /// The message flags to set. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the is invalid. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The does not support mod-sequences. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public virtual IList SetFlags (IList uids, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - return SetFlags (uids, modseq, flags, null, silent, cancellationToken); - } - - /// - /// Asynchronously set the flags of the specified messages only if their mod-sequence value is less than the specified value. - /// - /// - /// Asynchronously sets the flags of the specified messages only if their mod-sequence value is less than the specified value. - /// - /// The unique IDs of the messages that were not updated. - /// The UIDs of the messages. - /// The mod-sequence value. - /// The message flags to set. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the is invalid. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The does not support mod-sequences. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public virtual Task> SetFlagsAsync (IList uids, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - if (uids == null) - throw new ArgumentNullException (nameof (uids)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return SetFlags (uids, modseq, flags, silent, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Set the flags of the specified messages only if their mod-sequence value is less than the specified value. - /// - /// - /// Sets the flags of the specified messages only if their mod-sequence value is less than the specified value. - /// - /// The unique IDs of the messages that were not updated. - /// The UIDs of the messages. - /// The mod-sequence value. - /// The message flags to set. - /// A set of user-defined flags to set. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the is invalid. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The does not support mod-sequences. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public abstract IList SetFlags (IList uids, ulong modseq, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously set the flags of the specified messages only if their mod-sequence value is less than the specified value. - /// - /// - /// Asynchronously sets the flags of the specified messages only if their mod-sequence value is less than the specified value. - /// - /// The unique IDs of the messages that were not updated. - /// The UIDs of the messages. - /// The mod-sequence value. - /// The message flags to set. - /// A set of user-defined flags to set. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the is invalid. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The does not support mod-sequences. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public virtual Task> SetFlagsAsync (IList uids, ulong modseq, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - if (uids == null) - throw new ArgumentNullException (nameof (uids)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return SetFlags (uids, modseq, flags, userFlags, silent, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Add a set of flags to the specified message. - /// - /// - /// Adds a set of flags to the specified message. - /// - /// The index of the message. - /// The message flags to add. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is invalid. - /// -or- - /// No flags were specified. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public void AddFlags (int index, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - AddFlags (new [] { index }, flags, silent, cancellationToken); - } - - /// - /// Asynchronously add a set of flags to the specified message. - /// - /// - /// Asynchronously adds a set of flags to the specified message. - /// - /// An asynchronous task context. - /// The index of the messages. - /// The message flags to add. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is invalid. - /// -or- - /// No flags were specified. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public Task AddFlagsAsync (int index, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - return AddFlagsAsync (new [] { index }, flags, silent, cancellationToken); - } - - /// - /// Add a set of flags to the specified message. - /// - /// - /// Adds a set of flags to the specified message. - /// - /// The index of the message. - /// The message flags to add. - /// A set of user-defined flags to add. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is invalid. - /// -or- - /// No flags were specified. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public void AddFlags (int index, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - AddFlags (new [] { index }, flags, userFlags, silent, cancellationToken); - } - - /// - /// Asynchronously add a set of flags to the specified message. - /// - /// - /// Asynchronously adds a set of flags to the specified message. - /// - /// An asynchronous task context. - /// The index of the messages. - /// The message flags to add. - /// A set of user-defined flags to add. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is invalid. - /// -or- - /// No flags were specified. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public Task AddFlagsAsync (int index, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - return AddFlagsAsync (new [] { index }, flags, userFlags, silent, cancellationToken); - } - - /// - /// Add a set of flags to the specified messages. - /// - /// - /// Adds a set of flags to the specified messages. - /// - /// The indexes of the messages. - /// The message flags to add. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// No flags were specified. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public virtual void AddFlags (IList indexes, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - AddFlags (indexes, flags, null, silent, cancellationToken); - } - - /// - /// Asynchronously add a set of flags to the specified messages. - /// - /// - /// Asynchronously adds a set of flags to the specified messages. - /// - /// An asynchronous task context. - /// The indexes of the messages. - /// The message flags to add. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// No flags were specified. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public virtual Task AddFlagsAsync (IList indexes, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - if (indexes == null) - throw new ArgumentNullException (nameof (indexes)); - - if ((flags & SettableFlags) == 0) - throw new ArgumentException ("No flags were specified.", nameof (flags)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - AddFlags (indexes, flags, silent, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Add a set of flags to the specified messages. - /// - /// - /// Adds a set of flags to the specified messages. - /// - /// The indexes of the messages. - /// The message flags to add. - /// A set of user-defined flags to add. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// No flags were specified. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public abstract void AddFlags (IList indexes, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously add a set of flags to the specified messages. - /// - /// - /// Asynchronously adds a set of flags to the specified messages. - /// - /// An asynchronous task context. - /// The indexes of the messages. - /// The message flags to add. - /// A set of user-defined flags to add. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// No flags were specified. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public virtual Task AddFlagsAsync (IList indexes, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - if (indexes == null) - throw new ArgumentNullException (nameof (indexes)); - - if ((flags & SettableFlags) == 0 && (userFlags == null || userFlags.Count == 0)) - throw new ArgumentException ("No flags were specified.", nameof (flags)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - AddFlags (indexes, flags, userFlags, silent, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Remove a set of flags from the specified message. - /// - /// - /// Removes a set of flags from the specified message. - /// - /// The index of the message. - /// The message flags to remove. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is invalid. - /// -or- - /// No flags were specified. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public void RemoveFlags (int index, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - RemoveFlags (new [] { index }, flags, silent, cancellationToken); - } - - /// - /// Asynchronously remove a set of flags from the specified message. - /// - /// - /// Asynchronously removes a set of flags from the specified message. - /// - /// An asynchronous task context. - /// The index of the message. - /// The message flags to remove. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is invalid. - /// -or- - /// No flags were specified. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public Task RemoveFlagsAsync (int index, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - return RemoveFlagsAsync (new [] { index }, flags, silent, cancellationToken); - } - - /// - /// Remove a set of flags from the specified message. - /// - /// - /// Removes a set of flags from the specified message. - /// - /// The index of the message. - /// The message flags to remove. - /// A set of user-defined flags to remove. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is invalid. - /// -or- - /// No flags were specified. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public void RemoveFlags (int index, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - RemoveFlags (new [] { index }, flags, userFlags, silent, cancellationToken); - } - - /// - /// Asynchronously remove a set of flags from the specified message. - /// - /// - /// Asynchronously removes a set of flags from the specified message. - /// - /// An asynchronous task context. - /// The index of the message. - /// The message flags to remove. - /// A set of user-defined flags to remove. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is invalid. - /// -or- - /// No flags were specified. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public Task RemoveFlagsAsync (int index, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - return RemoveFlagsAsync (new [] { index }, flags, userFlags, silent, cancellationToken); - } - - /// - /// Remove a set of flags from the specified messages. - /// - /// - /// Removes a set of flags from the specified messages. - /// - /// The indexes of the messages. - /// The message flags to remove. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// No flags were specified. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public virtual void RemoveFlags (IList indexes, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - RemoveFlags (indexes, flags, null, silent, cancellationToken); - } - - /// - /// Asynchronously remove a set of flags from the specified messages. - /// - /// - /// Asynchronously removes a set of flags from the specified messages. - /// - /// An asynchronous task context. - /// The indexes of the messages. - /// The message flags to remove. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// No flags were specified. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public virtual Task RemoveFlagsAsync (IList indexes, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - if (indexes == null) - throw new ArgumentNullException (nameof (indexes)); - - if ((flags & SettableFlags) == 0) - throw new ArgumentException ("No flags were specified.", nameof (flags)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - RemoveFlags (indexes, flags, silent, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Remove a set of flags from the specified messages. - /// - /// - /// Removes a set of flags from the specified messages. - /// - /// The indexes of the messages. - /// The message flags to remove. - /// A set of user-defined flags to remove. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// No flags were specified. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public abstract void RemoveFlags (IList indexes, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously remove a set of flags from the specified messages. - /// - /// - /// Asynchronously removes a set of flags from the specified messages. - /// - /// An asynchronous task context. - /// The indexes of the messages. - /// The message flags to remove. - /// A set of user-defined flags to remove. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// No flags were specified. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public virtual Task RemoveFlagsAsync (IList indexes, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - if (indexes == null) - throw new ArgumentNullException (nameof (indexes)); - - if ((flags & SettableFlags) == 0 && (userFlags == null || userFlags.Count == 0)) - throw new ArgumentException ("No flags were specified.", nameof (flags)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - RemoveFlags (indexes, flags, userFlags, silent, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Set the flags of the specified message. - /// - /// - /// Sets the flags of the specified message. - /// - /// The index of the message. - /// The message flags to set. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is invalid. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public void SetFlags (int index, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - SetFlags (new [] { index }, flags, silent, cancellationToken); - } - - /// - /// Asynchronously set the flags of the specified message. - /// - /// - /// Asynchronously sets the flags of the specified message. - /// - /// An asynchronous task context. - /// The index of the message. - /// The message flags to set. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is invalid. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public Task SetFlagsAsync (int index, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - return SetFlagsAsync (new [] { index }, flags, silent, cancellationToken); - } - - /// - /// Set the flags of the specified message. - /// - /// - /// Sets the flags of the specified message. - /// - /// The index of the message. - /// The message flags to set. - /// A set of user-defined flags to set. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is invalid. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public void SetFlags (int index, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - SetFlags (new [] { index }, flags, userFlags, silent, cancellationToken); - } - - /// - /// Asynchronously set the flags of the specified message. - /// - /// - /// Asynchronously sets the flags of the specified message. - /// - /// An asynchronous task context. - /// The index of the message. - /// The message flags to set. - /// A set of user-defined flags to set. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is invalid. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public Task SetFlagsAsync (int index, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - return SetFlagsAsync (new [] { index }, flags, userFlags, silent, cancellationToken); - } - - /// - /// Set the flags of the specified messages. - /// - /// - /// Sets the flags of the specified messages. - /// - /// The indexes of the messages. - /// The message flags to set. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the is invalid. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public virtual void SetFlags (IList indexes, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - SetFlags (indexes, flags, null, silent, cancellationToken); - } - - /// - /// Asynchronously set the flags of the specified messages. - /// - /// - /// Asynchronously sets the flags of the specified messages. - /// - /// An asynchronous task context. - /// The indexes of the messages. - /// The message flags to set. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the is invalid. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public virtual Task SetFlagsAsync (IList indexes, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - if (indexes == null) - throw new ArgumentNullException (nameof (indexes)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - SetFlags (indexes, flags, silent, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Set the flags of the specified messages. - /// - /// - /// Sets the flags of the specified messages. - /// - /// The indexes of the messages. - /// The message flags to set. - /// A set of user-defined flags to set. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the is invalid. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public abstract void SetFlags (IList indexes, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously set the flags of the specified messages. - /// - /// - /// Asynchronously sets the flags of the specified messages. - /// - /// An asynchronous task context. - /// The indexes of the messages. - /// The message flags to set. - /// A set of user-defined flags to set. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the is invalid. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public virtual Task SetFlagsAsync (IList indexes, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - if (indexes == null) - throw new ArgumentNullException (nameof (indexes)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - SetFlags (indexes, flags, userFlags, silent, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Add a set of flags to the specified messages only if their mod-sequence value is less than the specified value. - /// - /// - /// Adds a set of flags to the specified messages only if their mod-sequence value is less than the specified value. - /// - /// The indexes of the messages that were not updated. - /// The indexes of the messages. - /// The mod-sequence value. - /// The message flags to add. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// No flags were specified. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The does not support mod-sequences. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public virtual IList AddFlags (IList indexes, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - return AddFlags (indexes, modseq, flags, null, silent, cancellationToken); - } - - /// - /// Asynchronously add a set of flags to the specified messages only if their mod-sequence value is less than the specified value. - /// - /// - /// Asynchronously adds a set of flags to the specified messages only if their mod-sequence value is less than the specified value. - /// - /// The indexes of the messages that were not updated. - /// The indexes of the messages. - /// The mod-sequence value. - /// The message flags to add. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// No flags were specified. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The does not support mod-sequences. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public virtual Task> AddFlagsAsync (IList indexes, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - if (indexes == null) - throw new ArgumentNullException (nameof (indexes)); - - if ((flags & SettableFlags) == 0) - throw new ArgumentException ("No flags were specified.", nameof (flags)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return AddFlags (indexes, modseq, flags, silent, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Add a set of flags to the specified messages only if their mod-sequence value is less than the specified value. - /// - /// - /// Adds a set of flags to the specified messages only if their mod-sequence value is less than the specified value. - /// - /// The indexes of the messages that were not updated. - /// The indexes of the messages. - /// The mod-sequence value. - /// The message flags to add. - /// A set of user-defined flags to add. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// No flags were specified. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The does not support mod-sequences. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public abstract IList AddFlags (IList indexes, ulong modseq, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously add a set of flags to the specified messages only if their mod-sequence value is less than the specified value. - /// - /// - /// Asynchronously adds a set of flags to the specified messages only if their mod-sequence value is less than the specified value. - /// - /// The indexes of the messages that were not updated. - /// The indexes of the messages. - /// The mod-sequence value. - /// The message flags to add. - /// A set of user-defined flags to add. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// No flags were specified. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The does not support mod-sequences. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public virtual Task> AddFlagsAsync (IList indexes, ulong modseq, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - if (indexes == null) - throw new ArgumentNullException (nameof (indexes)); - - if ((flags & SettableFlags) == 0 && (userFlags == null || userFlags.Count == 0)) - throw new ArgumentException ("No flags were specified.", nameof (flags)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return AddFlags (indexes, modseq, flags, userFlags, silent, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Remove a set of flags from the specified messages only if their mod-sequence value is less than the specified value. - /// - /// - /// Removes a set of flags from the specified messages only if their mod-sequence value is less than the specified value. - /// - /// The indexes of the messages that were not updated. - /// The indexes of the messages. - /// The mod-sequence value. - /// The message flags to remove. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// No flags were specified. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The does not support mod-sequences. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public virtual IList RemoveFlags (IList indexes, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - return RemoveFlags (indexes, modseq, flags, null, silent, cancellationToken); - } - - /// - /// Asynchronously remove a set of flags from the specified messages only if their mod-sequence value is less than the specified value. - /// - /// - /// Asynchronously removes a set of flags from the specified messages only if their mod-sequence value is less than the specified value. - /// - /// The indexes of the messages that were not updated. - /// The indexes of the messages. - /// The mod-sequence value. - /// The message flags to remove. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// No flags were specified. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The does not support mod-sequences. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public virtual Task> RemoveFlagsAsync (IList indexes, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - if (indexes == null) - throw new ArgumentNullException (nameof (indexes)); - - if ((flags & SettableFlags) == 0) - throw new ArgumentException ("No flags were specified.", nameof (flags)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return RemoveFlags (indexes, modseq, flags, silent, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Remove a set of flags from the specified messages only if their mod-sequence value is less than the specified value. - /// - /// - /// Removes a set of flags from the specified messages only if their mod-sequence value is less than the specified value. - /// - /// The indexes of the messages that were not updated. - /// The indexes of the messages. - /// The mod-sequence value. - /// The message flags to remove. - /// A set of user-defined flags to remove. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// No flags were specified. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The folder is not currently open in read-write mode. - /// - /// - /// The does not support mod-sequences. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public abstract IList RemoveFlags (IList indexes, ulong modseq, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously remove a set of flags from the specified messages only if their mod-sequence value is less than the specified value. - /// - /// - /// Asynchronously removes a set of flags from the specified messages only if their mod-sequence value is less than the specified value. - /// - /// The indexes of the messages that were not updated. - /// The indexes of the messages. - /// The mod-sequence value. - /// The message flags to remove. - /// A set of user-defined flags to remove. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// No flags were specified. + /// is out of range. /// /// /// The has been disposed. @@ -12541,10 +5692,10 @@ public int Count { /// The is not authenticated. /// /// - /// The folder is not currently open in read-write mode. + /// The folder is not currently open. /// - /// - /// The does not support mod-sequences. + /// + /// The did not return the requested message stream. /// /// /// The operation was canceled via the cancellation token. @@ -12558,38 +5709,33 @@ public int Count { /// /// The command failed. /// - public virtual Task> RemoveFlagsAsync (IList indexes, ulong modseq, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + public virtual Task GetStreamAsync (int index, CancellationToken cancellationToken = default, ITransferProgress? progress = null) { - if (indexes == null) - throw new ArgumentNullException (nameof (indexes)); - - if ((flags & SettableFlags) == 0 && (userFlags == null || userFlags.Count == 0)) - throw new ArgumentException ("No flags were specified.", nameof (flags)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return RemoveFlags (indexes, modseq, flags, userFlags, silent, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); + return GetStreamAsync (index, string.Empty, cancellationToken, progress); } /// - /// Set the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// Get a substream of the specified message. /// /// - /// Sets the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// Gets a substream of the message. If the starting offset is beyond + /// the end of the message, an empty stream is returned. If the number of + /// bytes desired extends beyond the end of the message, a truncated stream + /// will be returned. /// - /// The indexes of the messages that were not updated. - /// The indexes of the messages. - /// The mod-sequence value. - /// The message flags to set. - /// If set to true, no events will be emitted. + /// The stream. + /// The UID of the message. + /// The starting offset of the first desired byte. + /// The number of bytes desired. /// The cancellation token. - /// - /// is null. - /// + /// The progress reporting mechanism. /// - /// One or more of the is invalid. + /// is invalid. + /// + /// + /// is negative. + /// -or- + /// is negative. /// /// /// The has been disposed. @@ -12601,10 +5747,10 @@ public int Count { /// The is not authenticated. /// /// - /// The folder is not currently open in read-write mode. + /// The folder is not currently open. /// - /// - /// The does not support mod-sequences. + /// + /// The did not return the requested message stream. /// /// /// The operation was canceled via the cancellation token. @@ -12618,28 +5764,30 @@ public int Count { /// /// The command failed. /// - public virtual IList SetFlags (IList indexes, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - return SetFlags (indexes, modseq, flags, null, silent, cancellationToken); - } + public abstract Stream GetStream (UniqueId uid, int offset, int count, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Asynchronously set the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// Asynchronously get a substream of the specified message. /// /// - /// Asynchronously sets the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// Asynchronously gets a substream of the message. If the starting offset is beyond + /// the end of the message, an empty stream is returned. If the number of + /// bytes desired extends beyond the end of the message, a truncated stream + /// will be returned. /// - /// The indexes of the messages that were not updated. - /// The indexes of the messages. - /// The mod-sequence value. - /// The message flags to set. - /// If set to true, no events will be emitted. + /// The stream. + /// The UID of the message. + /// The starting offset of the first desired byte. + /// The number of bytes desired. /// The cancellation token. - /// - /// is null. - /// + /// The progress reporting mechanism. /// - /// One or more of the is invalid. + /// is invalid. + /// + /// + /// is negative. + /// -or- + /// is negative. /// /// /// The has been disposed. @@ -12651,10 +5799,10 @@ public int Count { /// The is not authenticated. /// /// - /// The folder is not currently open in read-write mode. + /// The folder is not currently open. /// - /// - /// The does not support mod-sequences. + /// + /// The did not return the requested message stream. /// /// /// The operation was canceled via the cancellation token. @@ -12668,36 +5816,29 @@ public int Count { /// /// The command failed. /// - public virtual Task> SetFlagsAsync (IList indexes, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - if (indexes == null) - throw new ArgumentNullException (nameof (indexes)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return SetFlags (indexes, modseq, flags, silent, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task GetStreamAsync (UniqueId uid, int offset, int count, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Set the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// Get a substream of the specified message. /// /// - /// Sets the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// Gets a substream of the message. If the starting offset is beyond + /// the end of the message, an empty stream is returned. If the number of + /// bytes desired extends beyond the end of the message, a truncated stream + /// will be returned. /// - /// The indexes of the messages that were not updated. - /// The indexes of the messages. - /// The mod-sequence value. - /// The message flags to set. - /// A set of user-defined flags to set. - /// If set to true, no events will be emitted. + /// The stream. + /// The index of the message. + /// The starting offset of the first desired byte. + /// The number of bytes desired. /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the is invalid. + /// The progress reporting mechanism. + /// + /// is out of range. + /// -or- + /// is negative. + /// -or- + /// is negative. /// /// /// The has been disposed. @@ -12709,10 +5850,10 @@ public int Count { /// The is not authenticated. /// /// - /// The folder is not currently open in read-write mode. + /// The folder is not currently open. /// - /// - /// The does not support mod-sequences. + /// + /// The did not return the requested message stream. /// /// /// The operation was canceled via the cancellation token. @@ -12726,26 +5867,29 @@ public int Count { /// /// The command failed. /// - public abstract IList SetFlags (IList indexes, ulong modseq, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + public abstract Stream GetStream (int index, int offset, int count, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Asynchronously set the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// Asynchronously get a substream of the specified message. /// /// - /// Asynchronously sets the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// Asynchronously gets a substream of the message. If the starting offset is beyond + /// the end of the message, an empty stream is returned. If the number of + /// bytes desired extends beyond the end of the message, a truncated stream + /// will be returned. /// - /// The indexes of the messages that were not updated. - /// The indexes of the messages. - /// The mod-sequence value. - /// The message flags to set. - /// A set of user-defined flags to set. - /// If set to true, no events will be emitted. + /// The stream. + /// The index of the message. + /// The starting offset of the first desired byte. + /// The number of bytes desired. /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the is invalid. + /// The progress reporting mechanism. + /// + /// is out of range. + /// -or- + /// is negative. + /// -or- + /// is negative. /// /// /// The has been disposed. @@ -12757,10 +5901,10 @@ public int Count { /// The is not authenticated. /// /// - /// The folder is not currently open in read-write mode. + /// The folder is not currently open. /// - /// - /// The does not support mod-sequences. + /// + /// The did not return the requested message stream. /// /// /// The operation was canceled via the cancellation token. @@ -12774,35 +5918,27 @@ public int Count { /// /// The command failed. /// - public virtual Task> SetFlagsAsync (IList indexes, ulong modseq, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - if (indexes == null) - throw new ArgumentNullException (nameof (indexes)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return SetFlags (indexes, modseq, flags, userFlags, silent, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task GetStreamAsync (int index, int offset, int count, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Add a set of labels to the specified message. + /// Get a body part as a stream. /// /// - /// Adds a set of labels to the specified message. + /// Gets a body part as a stream. /// + /// + /// + /// + /// The body part stream. /// The UID of the message. - /// The labels to add. - /// If set to true, no events will be emitted. + /// The desired body part. /// The cancellation token. - /// - /// is null. - /// + /// The progress reporting mechanism. /// - /// is invalid. - /// -or- - /// No labels were specified. + /// is invalid. + /// + /// + /// is . /// /// /// The has been disposed. @@ -12814,7 +5950,10 @@ public int Count { /// The is not authenticated. /// /// - /// The folder is not currently open in read-write mode. + /// The folder is not currently open. + /// + /// + /// The did not return the requested message stream. /// /// /// The operation was canceled via the cancellation token. @@ -12828,29 +5967,36 @@ public int Count { /// /// The command failed. /// - public void AddLabels (UniqueId uid, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + public virtual Stream GetStream (UniqueId uid, BodyPart part, CancellationToken cancellationToken = default, ITransferProgress? progress = null) { - AddLabels (new [] { uid }, labels, silent, cancellationToken); + if (!uid.IsValid) + throw new ArgumentException ("The uid is invalid.", nameof (uid)); + + if (part == null) + throw new ArgumentNullException (nameof (part)); + + return GetStream (uid, part.PartSpecifier, cancellationToken, progress); } /// - /// Asynchronously add a set of labels to the specified message. + /// Asynchronously get a body part as a stream. /// /// - /// Asynchronously adds a set of labels to the specified message. + /// Asynchronously gets a body part as a stream. /// - /// An asynchronous task context. + /// + /// + /// + /// The body part stream. /// The UID of the message. - /// The labels to add. - /// If set to true, no events will be emitted. + /// The desired body part. /// The cancellation token. - /// - /// is null. - /// + /// The progress reporting mechanism. /// - /// is invalid. - /// -or- - /// No labels were specified. + /// is invalid. + /// + /// + /// is . /// /// /// The has been disposed. @@ -12862,7 +6008,10 @@ public int Count { /// The is not authenticated. /// /// - /// The folder is not currently open in read-write mode. + /// The folder is not currently open. + /// + /// + /// The did not return the requested message stream. /// /// /// The operation was canceled via the cancellation token. @@ -12876,30 +6025,33 @@ public int Count { /// /// The command failed. /// - public Task AddLabelsAsync (UniqueId uid, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + public virtual Task GetStreamAsync (UniqueId uid, BodyPart part, CancellationToken cancellationToken = default, ITransferProgress? progress = null) { - return AddLabelsAsync (new [] { uid }, labels, silent, cancellationToken); + if (!uid.IsValid) + throw new ArgumentException ("The uid is invalid.", nameof (uid)); + + if (part == null) + throw new ArgumentNullException (nameof (part)); + + return GetStreamAsync (uid, part.PartSpecifier, cancellationToken, progress); } /// - /// Add a set of labels to the specified messages. + /// Get a body part as a stream. /// /// - /// Adds a set of labels to the specified messages. + /// Gets a body part as a stream. /// - /// The UIDs of the messages. - /// The labels to add. - /// If set to true, no events will be emitted. + /// The body part stream. + /// The index of the message. + /// The desired body part. /// The cancellation token. + /// The progress reporting mechanism. /// - /// is null. - /// -or- - /// is null. + /// is . /// - /// - /// One or more of the is invalid. - /// -or- - /// No labels were specified. + /// + /// is out of range. /// /// /// The has been disposed. @@ -12911,7 +6063,10 @@ public int Count { /// The is not authenticated. /// /// - /// The folder is not currently open in read-write mode. + /// The folder is not currently open. + /// + /// + /// The did not return the requested message stream. /// /// /// The operation was canceled via the cancellation token. @@ -12925,28 +6080,33 @@ public int Count { /// /// The command failed. /// - public abstract void AddLabels (IList uids, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + public virtual Stream GetStream (int index, BodyPart part, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException (nameof (index)); + + if (part == null) + throw new ArgumentNullException (nameof (part)); + + return GetStream (index, part.PartSpecifier, cancellationToken, progress); + } /// - /// Asynchronously add a set of labels to the specified messages. + /// Asynchronously get a body part as a stream. /// /// - /// Asynchronously adds a set of labels to the specified messages. + /// Asynchronously gets a body part as a stream. /// - /// An asynchronous task context. - /// The UIDs of the messages. - /// The labels to add. - /// If set to true, no events will be emitted. + /// The body part stream. + /// The index of the message. + /// The desired body part. /// The cancellation token. + /// The progress reporting mechanism. /// - /// is null. - /// -or- - /// is null. + /// is . /// - /// - /// One or more of the is invalid. - /// -or- - /// No labels were specified. + /// + /// is out of range. /// /// /// The has been disposed. @@ -12958,7 +6118,10 @@ public int Count { /// The is not authenticated. /// /// - /// The folder is not currently open in read-write mode. + /// The folder is not currently open. + /// + /// + /// The did not return the requested message stream. /// /// /// The operation was canceled via the cancellation token. @@ -12972,41 +6135,43 @@ public int Count { /// /// The command failed. /// - public virtual Task AddLabelsAsync (IList uids, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + public virtual Task GetStreamAsync (int index, BodyPart part, CancellationToken cancellationToken = default, ITransferProgress? progress = null) { - if (uids == null) - throw new ArgumentNullException (nameof (uids)); - - if (labels == null) - throw new ArgumentNullException (nameof (labels)); + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException (nameof (index)); - if (labels.Count == 0) - throw new ArgumentException ("No labels were specified.", nameof (labels)); + if (part == null) + throw new ArgumentNullException (nameof (part)); - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - AddLabels (uids, labels, silent, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); + return GetStreamAsync (index, part.PartSpecifier, cancellationToken, progress); } /// - /// Remove a set of labels from the specified message. + /// Get a substream of the specified body part. /// /// - /// Removes a set of labels from the specified message. + /// Gets a substream of the body part. If the starting offset is beyond + /// the end of the body part, an empty stream is returned. If the number of + /// bytes desired extends beyond the end of the body part, a truncated stream + /// will be returned. /// - /// The UIDs of the message. - /// The labels to remove. - /// If set to true, no events will be emitted. + /// The stream. + /// The UID of the message. + /// The desired body part. + /// The starting offset of the first desired byte. + /// The number of bytes desired. /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is invalid. + /// /// - /// is null. + /// is . /// - /// - /// is invalid. + /// + /// is negative. /// -or- - /// No labels were specified. + /// is negative. /// /// /// The has been disposed. @@ -13018,7 +6183,10 @@ public int Count { /// The is not authenticated. /// /// - /// The folder is not currently open in read-write mode. + /// The folder is not currently open. + /// + /// + /// The did not return the requested message stream. /// /// /// The operation was canceled via the cancellation token. @@ -13032,29 +6200,49 @@ public int Count { /// /// The command failed. /// - public void RemoveLabels (UniqueId uid, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + public virtual Stream GetStream (UniqueId uid, BodyPart part, int offset, int count, CancellationToken cancellationToken = default, ITransferProgress? progress = null) { - RemoveLabels (new [] { uid }, labels, silent, cancellationToken); + if (!uid.IsValid) + throw new ArgumentException ("The uid is invalid.", nameof (uid)); + + if (part == null) + throw new ArgumentNullException (nameof (part)); + + if (offset < 0) + throw new ArgumentOutOfRangeException (nameof (offset)); + + if (count < 0) + throw new ArgumentOutOfRangeException (nameof (count)); + + return GetStream (uid, part.PartSpecifier, offset, count, cancellationToken, progress); } /// - /// Asynchronously remove a set of labels from the specified message. + /// Asynchronously get a substream of the specified body part. /// /// - /// Asynchronously removes a set of labels from the specified message. + /// Asynchronously gets a substream of the body part. If the starting offset is beyond + /// the end of the body part, an empty stream is returned. If the number of + /// bytes desired extends beyond the end of the body part, a truncated stream + /// will be returned. /// - /// An asynchronous task context. + /// The stream. /// The UID of the message. - /// The labels to remove. - /// If set to true, no events will be emitted. + /// The desired body part. + /// The starting offset of the first desired byte. + /// The number of bytes desired. /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is invalid. + /// /// - /// is null. + /// is . /// - /// - /// is invalid. + /// + /// is negative. /// -or- - /// No labels were specified. + /// is negative. /// /// /// The has been disposed. @@ -13066,7 +6254,10 @@ public int Count { /// The is not authenticated. /// /// - /// The folder is not currently open in read-write mode. + /// The folder is not currently open. + /// + /// + /// The did not return the requested message stream. /// /// /// The operation was canceled via the cancellation token. @@ -13080,30 +6271,48 @@ public int Count { /// /// The command failed. /// - public Task RemoveLabelsAsync (UniqueId uid, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + public virtual Task GetStreamAsync (UniqueId uid, BodyPart part, int offset, int count, CancellationToken cancellationToken = default, ITransferProgress? progress = null) { - return RemoveLabelsAsync (new [] { uid }, labels, silent, cancellationToken); + if (!uid.IsValid) + throw new ArgumentException ("The uid is invalid.", nameof (uid)); + + if (part == null) + throw new ArgumentNullException (nameof (part)); + + if (offset < 0) + throw new ArgumentOutOfRangeException (nameof (offset)); + + if (count < 0) + throw new ArgumentOutOfRangeException (nameof (count)); + + return GetStreamAsync (uid, part.PartSpecifier, offset, count, cancellationToken, progress); } /// - /// Remove a set of labels from the specified messages. + /// Get a substream of the specified body part. /// /// - /// Removes a set of labels from the specified messages. + /// Gets a substream of the body part. If the starting offset is beyond + /// the end of the body part, an empty stream is returned. If the number of + /// bytes desired extends beyond the end of the body part, a truncated stream + /// will be returned. /// - /// The UIDs of the messages. - /// The labels to remove. - /// If set to true, no events will be emitted. + /// The stream. + /// The index of the message. + /// The desired body part. + /// The starting offset of the first desired byte. + /// The number of bytes desired. /// The cancellation token. + /// The progress reporting mechanism. /// - /// is null. - /// -or- - /// is null. + /// is . /// - /// - /// One or more of the is invalid. + /// + /// is out of range. + /// -or- + /// is negative. /// -or- - /// No labels were specified. + /// is negative. /// /// /// The has been disposed. @@ -13115,7 +6324,10 @@ public int Count { /// The is not authenticated. /// /// - /// The folder is not currently open in read-write mode. + /// The folder is not currently open. + /// + /// + /// The did not return the requested message stream. /// /// /// The operation was canceled via the cancellation token. @@ -13129,28 +6341,48 @@ public int Count { /// /// The command failed. /// - public abstract void RemoveLabels (IList uids, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + public virtual Stream GetStream (int index, BodyPart part, int offset, int count, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException (nameof (index)); + + if (part == null) + throw new ArgumentNullException (nameof (part)); + + if (offset < 0) + throw new ArgumentOutOfRangeException (nameof (offset)); + + if (count < 0) + throw new ArgumentOutOfRangeException (nameof (count)); + + return GetStream (index, part.PartSpecifier, offset, count, cancellationToken, progress); + } /// - /// Asynchronously remove a set of labels from the specified messages. + /// Asynchronously get a substream of the specified body part. /// /// - /// Asynchronously removes a set of labels from the specified messages. + /// Asynchronously gets a substream of the body part. If the starting offset is beyond + /// the end of the body part, an empty stream is returned. If the number of + /// bytes desired extends beyond the end of the body part, a truncated stream + /// will be returned. /// - /// An asynchronous task context. - /// The UIDs of the messages. - /// The labels to remove. - /// If set to true, no events will be emitted. + /// The stream. + /// The index of the message. + /// The desired body part. + /// The starting offset of the first desired byte. + /// The number of bytes desired. /// The cancellation token. + /// The progress reporting mechanism. /// - /// is null. - /// -or- - /// is null. + /// is . /// - /// - /// One or more of the is invalid. + /// + /// is out of range. /// -or- - /// No labels were specified. + /// is negative. + /// -or- + /// is negative. /// /// /// The has been disposed. @@ -13162,7 +6394,10 @@ public int Count { /// The is not authenticated. /// /// - /// The folder is not currently open in read-write mode. + /// The folder is not currently open. + /// + /// + /// The did not return the requested message stream. /// /// /// The operation was canceled via the cancellation token. @@ -13176,40 +6411,45 @@ public int Count { /// /// The command failed. /// - public virtual Task RemoveLabelsAsync (IList uids, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + public virtual Task GetStreamAsync (int index, BodyPart part, int offset, int count, CancellationToken cancellationToken = default, ITransferProgress? progress = null) { - if (uids == null) - throw new ArgumentNullException (nameof (uids)); + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException (nameof (index)); + + if (part == null) + throw new ArgumentNullException (nameof (part)); - if (labels == null) - throw new ArgumentNullException (nameof (labels)); + if (offset < 0) + throw new ArgumentOutOfRangeException (nameof (offset)); - if (labels.Count == 0) - throw new ArgumentException ("No labels were specified.", nameof (labels)); + if (count < 0) + throw new ArgumentOutOfRangeException (nameof (count)); - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - RemoveLabels (uids, labels, silent, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); + return GetStreamAsync (index, part.PartSpecifier, offset, count, cancellationToken, progress); } /// - /// Set the labels of the specified message. + /// Get a substream of the specified message. /// /// - /// Sets the labels of the specified message. + /// Gets a substream of the specified message. + /// For more information about how to construct the , + /// see Section 6.4.5 of RFC3501. /// - /// The UIDs of the message. - /// The labels to set. - /// If set to true, no events will be emitted. + /// + /// + /// + /// The stream. + /// The UID of the message. + /// The desired section of the message. /// The cancellation token. - /// - /// is null. - /// + /// The progress reporting mechanism. /// /// is invalid. /// + /// + /// is . + /// /// /// The has been disposed. /// @@ -13220,7 +6460,10 @@ public int Count { /// The is not authenticated. /// /// - /// The folder is not currently open in read-write mode. + /// The folder is not currently open. + /// + /// + /// The did not return the requested message stream. /// /// /// The operation was canceled via the cancellation token. @@ -13234,28 +6477,30 @@ public int Count { /// /// The command failed. /// - public void SetLabels (UniqueId uid, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - SetLabels (new [] { uid }, labels, silent, cancellationToken); - } + public abstract Stream GetStream (UniqueId uid, string section, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Asynchronously set the labels of the specified message. + /// Asynchronously get a substream of the specified message. /// /// - /// Asynchronously sets the labels of the specified message. + /// Asynchronously gets a substream of the specified message. + /// For more information about how to construct the , + /// see Section 6.4.5 of RFC3501. /// - /// An asynchronous task context. + /// + /// + /// + /// The stream. /// The UID of the message. - /// The labels to set. - /// If set to true, no events will be emitted. + /// The desired section of the message. /// The cancellation token. - /// - /// is null. - /// + /// The progress reporting mechanism. /// /// is invalid. /// + /// + /// is . + /// /// /// The has been disposed. /// @@ -13266,7 +6511,10 @@ public int Count { /// The is not authenticated. /// /// - /// The folder is not currently open in read-write mode. + /// The folder is not currently open. + /// + /// + /// The did not return the requested message stream. /// /// /// The operation was canceled via the cancellation token. @@ -13280,28 +6528,36 @@ public int Count { /// /// The command failed. /// - public Task SetLabelsAsync (UniqueId uid, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - return SetLabelsAsync (new [] { uid }, labels, silent, cancellationToken); - } + public abstract Task GetStreamAsync (UniqueId uid, string section, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Set the labels of the specified messages. + /// Get a substream of the specified message. /// /// - /// Sets the labels of the specified messages. + /// Gets a substream of the specified message. If the starting offset is beyond + /// the end of the specified section of the message, an empty stream is returned. If + /// the number of bytes desired extends beyond the end of the section, a truncated + /// stream will be returned. + /// For more information about how to construct the , + /// see Section 6.4.5 of RFC3501. /// - /// The UIDs of the messages. - /// The labels to set. - /// If set to true, no events will be emitted. + /// The stream. + /// The UID of the message. + /// The desired section of the message. + /// The starting offset of the first desired byte. + /// The number of bytes desired. /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is invalid. + /// /// - /// is null. - /// -or- - /// is null. + /// is . /// - /// - /// One or more of the is invalid. + /// + /// is negative. + /// -or- + /// is negative. /// /// /// The has been disposed. @@ -13313,7 +6569,10 @@ public int Count { /// The is not authenticated. /// /// - /// The folder is not currently open in read-write mode. + /// The folder is not currently open. + /// + /// + /// The did not return the requested message stream. /// /// /// The operation was canceled via the cancellation token. @@ -13327,26 +6586,36 @@ public int Count { /// /// The command failed. /// - public abstract void SetLabels (IList uids, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + public abstract Stream GetStream (UniqueId uid, string section, int offset, int count, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Asynchronously set the labels of the specified messages. + /// Asynchronously get a substream of the specified message. /// /// - /// Asynchronously sets the labels of the specified messages. + /// Asynchronously gets a substream of the specified message. If the starting + /// offset is beyond the end of the specified section of the message, an empty stream + /// is returned. If the number of bytes desired extends beyond the end of the section, + /// a truncated stream will be returned. + /// For more information about how to construct the , + /// see Section 6.4.5 of RFC3501. /// - /// An asynchronous task context. - /// The UIDs of the messages. - /// The labels to set. - /// If set to true, no events will be emitted. + /// The stream. + /// The UID of the message. + /// The desired section of the message. + /// The starting offset of the first desired byte. + /// The number of bytes desired. /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is invalid. + /// /// - /// is null. - /// -or- - /// is null. + /// is . /// - /// - /// One or more of the is invalid. + /// + /// is negative. + /// -or- + /// is negative. /// /// /// The has been disposed. @@ -13358,7 +6627,10 @@ public int Count { /// The is not authenticated. /// /// - /// The folder is not currently open in read-write mode. + /// The folder is not currently open. + /// + /// + /// The did not return the requested message stream. /// /// /// The operation was canceled via the cancellation token. @@ -13372,42 +6644,26 @@ public int Count { /// /// The command failed. /// - public virtual Task SetLabelsAsync (IList uids, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - if (uids == null) - throw new ArgumentNullException (nameof (uids)); - - if (labels == null) - throw new ArgumentNullException (nameof (labels)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - SetLabels (uids, labels, silent, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task GetStreamAsync (UniqueId uid, string section, int offset, int count, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Add a set of labels to the specified messages only if their mod-sequence value is less than the specified value. + /// Get a substream of the specified message. /// /// - /// Adds a set of labels to the specified messages only if their mod-sequence value is less than the specified value. + /// Gets a substream of the specified message. + /// For more information about how to construct the , + /// see Section 6.4.5 of RFC3501. /// - /// The unique IDs of the messages that were not updated. - /// The UIDs of the messages. - /// The mod-sequence value. - /// The labels to add. - /// If set to true, no events will be emitted. + /// The stream. + /// The index of the message. + /// The desired section of the message. /// The cancellation token. + /// The progress reporting mechanism. /// - /// is null. - /// -or- - /// is null. + /// is . /// - /// - /// One or more of the is invalid. - /// -or- - /// No labels were specified. + /// + /// is out of range. /// /// /// The has been disposed. @@ -13419,10 +6675,10 @@ public int Count { /// The is not authenticated. /// /// - /// The folder is not currently open in read-write mode. + /// The folder is not currently open. /// - /// - /// The does not support mod-sequences. + /// + /// The did not return the requested message stream. /// /// /// The operation was canceled via the cancellation token. @@ -13436,29 +6692,26 @@ public int Count { /// /// The command failed. /// - public abstract IList AddLabels (IList uids, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + public abstract Stream GetStream (int index, string section, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Asynchronously add a set of labels to the specified messages only if their mod-sequence value is less than the specified value. + /// Asynchronously get a substream of the specified body part. /// /// - /// Asynchronously adds a set of labels to the specified messages only if their mod-sequence value is less than the specified value. + /// Asynchronously gets a substream of the specified message. + /// For more information about how to construct the , + /// see Section 6.4.5 of RFC3501. /// - /// The unique IDs of the messages that were not updated. - /// The UIDs of the messages. - /// The mod-sequence value. - /// The labels to add. - /// If set to true, no events will be emitted. + /// The stream. + /// The index of the message. + /// The desired section of the message. /// The cancellation token. + /// The progress reporting mechanism. /// - /// is null. - /// -or- - /// is null. + /// is . /// - /// - /// One or more of the is invalid. - /// -or- - /// No labels were specified. + /// + /// is out of range. /// /// /// The has been disposed. @@ -13470,10 +6723,10 @@ public int Count { /// The is not authenticated. /// /// - /// The folder is not currently open in read-write mode. + /// The folder is not currently open. /// - /// - /// The does not support mod-sequences. + /// + /// The did not return the requested message stream. /// /// /// The operation was canceled via the cancellation token. @@ -13487,45 +6740,35 @@ public int Count { /// /// The command failed. /// - public virtual Task> AddLabelsAsync (IList uids, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - if (uids == null) - throw new ArgumentNullException (nameof (uids)); - - if (labels == null) - throw new ArgumentNullException (nameof (labels)); - - if (labels.Count == 0) - throw new ArgumentException ("No labels were specified.", nameof (labels)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return AddLabels (uids, modseq, labels, silent, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task GetStreamAsync (int index, string section, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Remove a set of labels from the specified messages only if their mod-sequence value is less than the specified value. + /// Get a substream of the specified message. /// /// - /// Removes a set of labels from the specified messages only if their mod-sequence value is less than the specified value. + /// Gets a substream of the specified message. If the starting offset is beyond + /// the end of the specified section of the message, an empty stream is returned. If + /// the number of bytes desired extends beyond the end of the section, a truncated + /// stream will be returned. + /// For more information about how to construct the , + /// see Section 6.4.5 of RFC3501. /// - /// The unique IDs of the messages that were not updated. - /// The UIDs of the messages. - /// The mod-sequence value. - /// The labels to remove. - /// If set to true, no events will be emitted. + /// The stream. + /// The index of the message. + /// The desired section of the message. + /// The starting offset of the first desired byte. + /// The number of bytes desired. /// The cancellation token. + /// The progress reporting mechanism. /// - /// is null. - /// -or- - /// is null. + /// is . /// - /// - /// One or more of the is invalid. + /// + /// is out of range. /// -or- - /// No labels were specified. + /// is negative. + /// -or- + /// is negative. /// /// /// The has been disposed. @@ -13537,10 +6780,10 @@ public int Count { /// The is not authenticated. /// /// - /// The folder is not currently open in read-write mode. + /// The folder is not currently open. /// - /// - /// The does not support mod-sequences. + /// + /// The did not return the requested message stream. /// /// /// The operation was canceled via the cancellation token. @@ -13554,29 +6797,35 @@ public int Count { /// /// The command failed. /// - public abstract IList RemoveLabels (IList uids, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + public abstract Stream GetStream (int index, string section, int offset, int count, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Asynchronously remove a set of labels from the specified messages only if their mod-sequence value is less than the specified value. + /// Asynchronously get a substream of the specified body part. /// /// - /// Asynchronously removes a set of labels from the specified messages only if their mod-sequence value is less than the specified value. + /// Asynchronously gets a substream of the specified message. If the starting + /// offset is beyond the end of the specified section of the message, an empty stream + /// is returned. If the number of bytes desired extends beyond the end of the section, + /// a truncated stream will be returned. + /// For more information about how to construct the , + /// see Section 6.4.5 of RFC3501. /// - /// The unique IDs of the messages that were not updated. - /// The UIDs of the messages. - /// The mod-sequence value. - /// The labels to remove. - /// If set to true, no events will be emitted. + /// The stream. + /// The index of the message. + /// The desired section of the message. + /// The starting offset of the first desired byte. + /// The number of bytes desired. /// The cancellation token. + /// The progress reporting mechanism. /// - /// is null. - /// -or- - /// is null. + /// is . /// - /// - /// One or more of the is invalid. + /// + /// is out of range. + /// -or- + /// is negative. /// -or- - /// No labels were specified. + /// is negative. /// /// /// The has been disposed. @@ -13588,10 +6837,10 @@ public int Count { /// The is not authenticated. /// /// - /// The folder is not currently open in read-write mode. + /// The folder is not currently open. /// - /// - /// The does not support mod-sequences. + /// + /// The did not return the requested message stream. /// /// /// The operation was canceled via the cancellation token. @@ -13605,43 +6854,23 @@ public int Count { /// /// The command failed. /// - public virtual Task> RemoveLabelsAsync (IList uids, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - if (uids == null) - throw new ArgumentNullException (nameof (uids)); - - if (labels == null) - throw new ArgumentNullException (nameof (labels)); - - if (labels.Count == 0) - throw new ArgumentException ("No labels were specified.", nameof (labels)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return RemoveLabels (uids, modseq, labels, silent, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task GetStreamAsync (int index, string section, int offset, int count, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Set the labels of the specified messages only if their mod-sequence value is less than the specified value. + /// Store message flags and keywords for a message. /// /// - /// Sets the labels of the specified messages only if their mod-sequence value is less than the specified value. + /// Updates the message flags and keywords for a message. /// - /// The unique IDs of the messages that were not updated. - /// The UIDs of the messages. - /// The mod-sequence value. - /// The labels to set. - /// If set to true, no events will be emitted. + /// if the store operation was successful; otherwise, . + /// The UID of the message. + /// The message flags and keywords to store. /// The cancellation token. - /// - /// is null. - /// -or- - /// is null. - /// /// - /// One or more of the is invalid. + /// is invalid. + /// + /// + /// is . /// /// /// The has been disposed. @@ -13656,7 +6885,8 @@ public int Count { /// The folder is not currently open in read-write mode. /// /// - /// The does not support mod-sequences. + /// The specified an + /// value but the folder does not support mod-sequences. /// /// /// The operation was canceled via the cancellation token. @@ -13670,27 +6900,28 @@ public int Count { /// /// The command failed. /// - public abstract IList SetLabels (IList uids, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + public virtual bool Store (UniqueId uid, IStoreFlagsRequest request, CancellationToken cancellationToken = default) + { + var uids = Store (new[] { uid }, request, cancellationToken); + + return uids == null || uids.Count == 0; + } /// - /// Asynchronously set the labels of the specified messages only if their mod-sequence value is less than the specified value. + /// Asynchronously store message flags and keywords for a message. /// /// - /// Asynchronously sets the labels of the specified messages only if their mod-sequence value is less than the specified value. + /// Asynchronously updates the message flags and keywords for a message. /// - /// The unique IDs of the messages that were not updated. - /// The UIDs of the messages. - /// The mod-sequence value. - /// The labels to set. - /// If set to true, no events will be emitted. + /// if the store operation was successful; otherwise, . + /// The UID of the message. + /// The message flags and keywords to store. /// The cancellation token. - /// - /// is null. - /// -or- - /// is null. - /// /// - /// One or more of the is invalid. + /// is invalid. + /// + /// + /// is . /// /// /// The has been disposed. @@ -13705,7 +6936,8 @@ public int Count { /// The folder is not currently open in read-write mode. /// /// - /// The does not support mod-sequences. + /// The specified an + /// value but the folder does not support mod-sequences. /// /// /// The operation was canceled via the cancellation token. @@ -13719,38 +6951,30 @@ public int Count { /// /// The command failed. /// - public virtual Task> SetLabelsAsync (IList uids, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + public virtual async Task StoreAsync (UniqueId uid, IStoreFlagsRequest request, CancellationToken cancellationToken = default) { - if (uids == null) - throw new ArgumentNullException (nameof (uids)); - - if (labels == null) - throw new ArgumentNullException (nameof (labels)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return SetLabels (uids, modseq, labels, silent, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); + var uids = await StoreAsync (new[] { uid }, request, cancellationToken).ConfigureAwait (false); + + return uids == null || uids.Count == 0; } /// - /// Add a set of labels to the specified message. + /// Store message flags and keywords for a set of messages. /// /// - /// Adds a set of labels to the specified message. + /// Updates the message flags and keywords for a set of messages. /// - /// The index of the message. - /// The labels to add. - /// If set to true, no events will be emitted. + /// The UIDs of the messages that were not updated. + /// The message UIDs. + /// The message flags and keywords to store. /// The cancellation token. /// - /// is null. + /// is . + /// -or- + /// is . /// /// - /// is invalid. - /// -or- - /// No labels were specified. + /// One or more of the is invalid. /// /// /// The has been disposed. @@ -13764,6 +6988,10 @@ public int Count { /// /// The folder is not currently open in read-write mode. /// + /// + /// The specified an + /// value but the folder does not support mod-sequences. + /// /// /// The operation was canceled via the cancellation token. /// @@ -13776,29 +7004,25 @@ public int Count { /// /// The command failed. /// - public void AddLabels (int index, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - AddLabels (new [] { index }, labels, silent, cancellationToken); - } + public abstract IList Store (IList uids, IStoreFlagsRequest request, CancellationToken cancellationToken = default); /// - /// Asynchronously add a set of labels to the specified message. + /// Asynchronously store message flags and keywords for a set of messages. /// /// - /// Asynchronously adds a set of labels to the specified message. + /// Asynchronously updates the message flags and keywords for a set of messages. /// - /// An asynchronous task context. - /// The index of the messages. - /// The labels to add. - /// If set to true, no events will be emitted. + /// The UIDs of the messages that were not updated. + /// The message UIDs. + /// The message flags and keywords to store. /// The cancellation token. /// - /// is null. + /// is . + /// -or- + /// is . /// /// - /// is invalid. - /// -or- - /// No labels were specified. + /// One or more of the is invalid. /// /// /// The has been disposed. @@ -13812,6 +7036,10 @@ public int Count { /// /// The folder is not currently open in read-write mode. /// + /// + /// The specified an + /// value but the folder does not support mod-sequences. + /// /// /// The operation was canceled via the cancellation token. /// @@ -13824,30 +7052,23 @@ public int Count { /// /// The command failed. /// - public Task AddLabelsAsync (int index, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - return AddLabelsAsync (new [] { index }, labels, silent, cancellationToken); - } + public abstract Task> StoreAsync (IList uids, IStoreFlagsRequest request, CancellationToken cancellationToken = default); /// - /// Add a set of labels to the specified messages. + /// Store message flags and keywords for a message. /// /// - /// Adds a set of labels to the specified messages. + /// Updates the message flags and keywords for a message. /// - /// The indexes of the messages. - /// The labels to add. - /// If set to true, no events will be emitted. + /// if the store operation was successful; otherwise, . + /// The index of the message. + /// The message flags and keywords to store. /// The cancellation token. - /// - /// is null. - /// -or- - /// is null. - /// /// - /// One or more of the is invalid. - /// -or- - /// No labels were specified. + /// is invalid. + /// + /// + /// is . /// /// /// The has been disposed. @@ -13861,6 +7082,10 @@ public int Count { /// /// The folder is not currently open in read-write mode. /// + /// + /// The specified an + /// value but the folder does not support mod-sequences. + /// /// /// The operation was canceled via the cancellation token. /// @@ -13873,28 +7098,28 @@ public int Count { /// /// The command failed. /// - public abstract void AddLabels (IList indexes, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + public virtual bool Store (int index, IStoreFlagsRequest request, CancellationToken cancellationToken = default) + { + var indexes = Store (new[] { index }, request, cancellationToken); + + return indexes == null || indexes.Count == 0; + } /// - /// Asynchronously add a set of labels to the specified messages. + /// Asynchronously store message flags and keywords for a message. /// /// - /// Asynchronously adds a set of labels to the specified messages. + /// Asynchronously updates the message flags and keywords for a message. /// - /// An asynchronous task context. - /// The indexes of the messages. - /// The labels to add. - /// If set to true, no events will be emitted. + /// if the store operation was successful; otherwise, . + /// The index of the message. + /// The message flags and keywords to store. /// The cancellation token. - /// - /// is null. - /// -or- - /// is null. - /// /// - /// One or more of the is invalid. - /// -or- - /// No labels were specified. + /// is invalid. + /// + /// + /// is . /// /// /// The has been disposed. @@ -13908,6 +7133,10 @@ public int Count { /// /// The folder is not currently open in read-write mode. /// + /// + /// The specified an + /// value but the folder does not support mod-sequences. + /// /// /// The operation was canceled via the cancellation token. /// @@ -13920,41 +7149,30 @@ public int Count { /// /// The command failed. /// - public virtual Task AddLabelsAsync (IList indexes, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + public virtual async Task StoreAsync (int index, IStoreFlagsRequest request, CancellationToken cancellationToken = default) { - if (indexes == null) - throw new ArgumentNullException (nameof (indexes)); - - if (labels == null) - throw new ArgumentNullException (nameof (labels)); + var indexes = await StoreAsync (new[] { index }, request, cancellationToken).ConfigureAwait (false); - if (labels.Count == 0) - throw new ArgumentException ("No labels were specified.", nameof (labels)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - AddLabels (indexes, labels, silent, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); + return indexes == null || indexes.Count == 0; } /// - /// Remove a set of labels from the specified message. + /// Store message flags and keywords for a set of messages. /// /// - /// Removes a set of labels from the specified message. + /// Updates the message flags and keywords for a set of messages. /// - /// The index of the message. - /// The labels to remove. - /// If set to true, no events will be emitted. + /// The indexes of the messages that were not updated. + /// The message indexes. + /// The message flags and keywords to store. /// The cancellation token. /// - /// is null. + /// is . + /// -or- + /// is . /// /// - /// is invalid. - /// -or- - /// No labels were specified. + /// One or more of the is invalid. /// /// /// The has been disposed. @@ -13968,6 +7186,10 @@ public int Count { /// /// The folder is not currently open in read-write mode. /// + /// + /// The specified an + /// value but the folder does not support mod-sequences. + /// /// /// The operation was canceled via the cancellation token. /// @@ -13980,29 +7202,25 @@ public int Count { /// /// The command failed. /// - public void RemoveLabels (int index, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - RemoveLabels (new [] { index }, labels, silent, cancellationToken); - } + public abstract IList Store (IList indexes, IStoreFlagsRequest request, CancellationToken cancellationToken = default); /// - /// Asynchronously remove a set of labels from the specified message. + /// Asynchronously store message flags and keywords for a set of messages. /// /// - /// Asynchronously removes a set of labels from the specified message. + /// Asynchronously updates the message flags and keywords for a set of messages. /// - /// An asynchronous task context. - /// The index of the message. - /// The labels to remove. - /// If set to true, no events will be emitted. + /// The indexes of the messages that were not updated. + /// The message indexes. + /// The message flags and keywords to store. /// The cancellation token. /// - /// is null. + /// is . + /// -or- + /// is . /// /// - /// is invalid. - /// -or- - /// No labels were specified. + /// One or more of the is invalid. /// /// /// The has been disposed. @@ -14016,6 +7234,10 @@ public int Count { /// /// The folder is not currently open in read-write mode. /// + /// + /// The specified an + /// value but the folder does not support mod-sequences. + /// /// /// The operation was canceled via the cancellation token. /// @@ -14028,30 +7250,23 @@ public int Count { /// /// The command failed. /// - public Task RemoveLabelsAsync (int index, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - return RemoveLabelsAsync (new [] { index }, labels, silent, cancellationToken); - } + public abstract Task> StoreAsync (IList indexes, IStoreFlagsRequest request, CancellationToken cancellationToken = default); /// - /// Remove a set of labels from the specified messages. + /// Store GMail-style labels for a message. /// /// - /// Removes a set of labels from the specified messages. + /// Updates the GMail-style labels for a message. /// - /// The indexes of the messages. - /// The labels to remove. - /// If set to true, no events will be emitted. + /// if the store operation was successful; otherwise, . + /// The UID of the message. + /// The GMail-style labels to store. /// The cancellation token. - /// - /// is null. - /// -or- - /// is null. - /// /// - /// One or more of the is invalid. - /// -or- - /// No labels were specified. + /// is invalid. + /// + /// + /// is . /// /// /// The has been disposed. @@ -14065,6 +7280,12 @@ public int Count { /// /// The folder is not currently open in read-write mode. /// + /// + /// The folder does not support storing labels. + /// -or- + /// The specified an + /// value but the folder does not support mod-sequences. + /// /// /// The operation was canceled via the cancellation token. /// @@ -14077,28 +7298,28 @@ public int Count { /// /// The command failed. /// - public abstract void RemoveLabels (IList indexes, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + public virtual bool Store (UniqueId uid, IStoreLabelsRequest request, CancellationToken cancellationToken = default) + { + var uids = Store (new[] { uid }, request, cancellationToken); + + return uids == null || uids.Count == 0; + } /// - /// Asynchronously remove a set of labels from the specified messages. + /// Asynchronously store GMail-style labels for a message. /// /// - /// Asynchronously removes a set of labels from the specified messages. + /// Asynchronously updates the GMail-style labels for a message. /// - /// An asynchronous task context. - /// The indexes of the messages. - /// The labels to remove. - /// If set to true, no events will be emitted. + /// if the store operation was successful; otherwise, . + /// The UID of the message. + /// The GMail-style labels to store. /// The cancellation token. - /// - /// is null. - /// -or- - /// is null. - /// /// - /// One or more of the is invalid. - /// -or- - /// No labels were specified. + /// is invalid. + /// + /// + /// is . /// /// /// The has been disposed. @@ -14112,6 +7333,12 @@ public int Count { /// /// The folder is not currently open in read-write mode. /// + /// + /// The folder does not support storing labels. + /// -or- + /// The specified an + /// value but the folder does not support mod-sequences. + /// /// /// The operation was canceled via the cancellation token. /// @@ -14124,39 +7351,30 @@ public int Count { /// /// The command failed. /// - public virtual Task RemoveLabelsAsync (IList indexes, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + public virtual async Task StoreAsync (UniqueId uid, IStoreLabelsRequest request, CancellationToken cancellationToken = default) { - if (indexes == null) - throw new ArgumentNullException (nameof (indexes)); - - if (labels == null) - throw new ArgumentNullException (nameof (labels)); - - if (labels.Count == 0) - throw new ArgumentException ("No labels were specified.", nameof (labels)); + var uids = await StoreAsync (new[] { uid }, request, cancellationToken).ConfigureAwait (false); - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - RemoveLabels (indexes, labels, silent, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); + return uids == null || uids.Count == 0; } /// - /// Set the labels of the specified message. + /// Store GMail-style labels for a set of messages. /// /// - /// Sets the labels of the specified message. + /// Updates the GMail-style labels for a set of messages. /// - /// The index of the message. - /// The labels to set. - /// If set to true, no events will be emitted. + /// The UIDs of the messages that were not updated. + /// The message UIDs. + /// The GMail-style labels to store. /// The cancellation token. /// - /// is null. + /// is . + /// -or- + /// is . /// /// - /// is invalid. + /// One or more of the is invalid. /// /// /// The has been disposed. @@ -14170,6 +7388,12 @@ public int Count { /// /// The folder is not currently open in read-write mode. /// + /// + /// The folder does not support storing labels. + /// -or- + /// The specified an + /// value but the folder does not support mod-sequences. + /// /// /// The operation was canceled via the cancellation token. /// @@ -14182,27 +7406,25 @@ public int Count { /// /// The command failed. /// - public void SetLabels (int index, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - SetLabels (new [] { index }, labels, silent, cancellationToken); - } + public abstract IList Store (IList uids, IStoreLabelsRequest request, CancellationToken cancellationToken = default); /// - /// Asynchronously set the labels of the specified message. + /// Asynchronously store GMail-style labels for a set of messages. /// /// - /// Asynchronously sets the labels of the specified message. + /// Asynchronously updates the GMail-style labels for a set of messages. /// - /// An asynchronous task context. - /// The index of the message. - /// The labels to set. - /// If set to true, no events will be emitted. + /// The UIDs of the messages that were not updated. + /// The message UIDs. + /// The GMail-style labels to store. /// The cancellation token. /// - /// is null. + /// is . + /// -or- + /// is . /// /// - /// is invalid. + /// One or more of the is invalid. /// /// /// The has been disposed. @@ -14216,6 +7438,12 @@ public int Count { /// /// The folder is not currently open in read-write mode. /// + /// + /// The folder does not support storing labels. + /// -or- + /// The specified an + /// value but the folder does not support mod-sequences. + /// /// /// The operation was canceled via the cancellation token. /// @@ -14228,28 +7456,23 @@ public int Count { /// /// The command failed. /// - public Task SetLabelsAsync (int index, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - return SetLabelsAsync (new [] { index }, labels, silent, cancellationToken); - } + public abstract Task> StoreAsync (IList uids, IStoreLabelsRequest request, CancellationToken cancellationToken = default); /// - /// Set the labels of the specified messages. + /// Store GMail-style labels for a message. /// /// - /// Sets the labels of the specified messages. + /// Updates the GMail-style labels for a message. /// - /// The indexes of the messages. - /// The labels to set. - /// If set to true, no events will be emitted. + /// if the store operation was successful; otherwise, . + /// The index of the message. + /// The GMail-style labels to store. /// The cancellation token. - /// - /// is null. - /// -or- - /// is null. - /// /// - /// One or more of the is invalid. + /// is invalid. + /// + /// + /// is . /// /// /// The has been disposed. @@ -14263,6 +7486,12 @@ public int Count { /// /// The folder is not currently open in read-write mode. /// + /// + /// The folder does not support storing labels. + /// -or- + /// The specified an + /// value but the folder does not support mod-sequences. + /// /// /// The operation was canceled via the cancellation token. /// @@ -14275,26 +7504,28 @@ public int Count { /// /// The command failed. /// - public abstract void SetLabels (IList indexes, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + public virtual bool Store (int index, IStoreLabelsRequest request, CancellationToken cancellationToken = default) + { + var indexes = Store (new[] { index }, request, cancellationToken); + + return indexes == null || indexes.Count == 0; + } /// - /// Asynchronously set the labels of the specified messages. + /// Asynchronously store GMail-style labels for a message. /// /// - /// Asynchronously sets the labels of the specified messages. + /// Asynchronously updates the GMail-style labels for a message. /// - /// An asynchronous task context. - /// The indexes of the messages. - /// The labels to set. - /// If set to true, no events will be emitted. + /// if the store operation was successful; otherwise, . + /// The index of the message. + /// The GMail-style labels to store. /// The cancellation token. - /// - /// is null. - /// -or- - /// is null. - /// /// - /// One or more of the is invalid. + /// is invalid. + /// + /// + /// is . /// /// /// The has been disposed. @@ -14308,6 +7539,12 @@ public int Count { /// /// The folder is not currently open in read-write mode. /// + /// + /// The folder does not support storing labels. + /// -or- + /// The specified an + /// value but the folder does not support mod-sequences. + /// /// /// The operation was canceled via the cancellation token. /// @@ -14320,42 +7557,30 @@ public int Count { /// /// The command failed. /// - public virtual Task SetLabelsAsync (IList indexes, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + public virtual async Task StoreAsync (int index, IStoreLabelsRequest request, CancellationToken cancellationToken = default) { - if (indexes == null) - throw new ArgumentNullException (nameof (indexes)); - - if (labels == null) - throw new ArgumentNullException (nameof (labels)); + var indexes = await StoreAsync (new[] { index }, request, cancellationToken).ConfigureAwait (false); - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - SetLabels (indexes, labels, silent, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); + return indexes == null || indexes.Count == 0; } /// - /// Add a set of labels to the specified messages only if their mod-sequence value is less than the specified value. + /// Store GMail-style labels for a set of messages. /// /// - /// Adds a set of labels to the specified messages only if their mod-sequence value is less than the specified value. + /// Updates the GMail-style labels for a set of messages. /// /// The indexes of the messages that were not updated. - /// The indexes of the messages. - /// The mod-sequence value. - /// The labels to add. - /// If set to true, no events will be emitted. + /// The message indexes. + /// The GMail-style labels to store. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// - /// One or more of the is invalid. - /// -or- - /// No labels were specified. + /// One or more of the is invalid. /// /// /// The has been disposed. @@ -14370,7 +7595,10 @@ public int Count { /// The folder is not currently open in read-write mode. /// /// - /// The does not support mod-sequences. + /// The folder does not support storing labels. + /// -or- + /// The specified an + /// value but the folder does not support mod-sequences. /// /// /// The operation was canceled via the cancellation token. @@ -14384,29 +7612,25 @@ public int Count { /// /// The command failed. /// - public abstract IList AddLabels (IList indexes, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + public abstract IList Store (IList indexes, IStoreLabelsRequest request, CancellationToken cancellationToken = default); /// - /// Asynchronously add a set of labels to the specified messages only if their mod-sequence value is less than the specified value. + /// Asynchronously store GMail-style labels for a set of messages. /// /// - /// Asynchronously adds a set of labels to the specified messages only if their mod-sequence value is less than the specified value. + /// Asynchronously updates the GMail-style labels for a set of messages. /// /// The indexes of the messages that were not updated. - /// The indexes of the messages. - /// The mod-sequence value. - /// The labels to add. - /// If set to true, no events will be emitted. + /// The message indexes. + /// The GMail-style labels to store. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// - /// One or more of the is invalid. - /// -or- - /// No labels were specified. + /// One or more of the is invalid. /// /// /// The has been disposed. @@ -14421,7 +7645,10 @@ public int Count { /// The folder is not currently open in read-write mode. /// /// - /// The does not support mod-sequences. + /// The folder does not support storing labels. + /// -or- + /// The specified an + /// value but the folder does not support mod-sequences. /// /// /// The operation was canceled via the cancellation token. @@ -14435,45 +7662,22 @@ public int Count { /// /// The command failed. /// - public virtual Task> AddLabelsAsync (IList indexes, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - if (indexes == null) - throw new ArgumentNullException (nameof (indexes)); - - if (labels == null) - throw new ArgumentNullException (nameof (labels)); - - if (labels.Count == 0) - throw new ArgumentException ("No labels were specified.", nameof (labels)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return AddLabels (indexes, modseq, labels, silent, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task> StoreAsync (IList indexes, IStoreLabelsRequest request, CancellationToken cancellationToken = default); /// - /// Remove a set of labels from the specified messages only if their mod-sequence value is less than the specified value. + /// Store the annotations for the specified message. /// /// - /// Removes a set of labels from the specified messages only if their mod-sequence value is less than the specified value. + /// Stores the annotations for the specified message. /// - /// The indexes of the messages that were not updated. - /// The indexes of the messages. - /// The mod-sequence value. - /// The labels to remove. - /// If set to true, no events will be emitted. + /// The UID of the message. + /// The annotations to store. /// The cancellation token. /// - /// is null. - /// -or- - /// is null. + /// is . /// /// - /// One or more of the is invalid. - /// -or- - /// No labels were specified. + /// is invalid. /// /// /// The has been disposed. @@ -14485,10 +7689,13 @@ public int Count { /// The is not authenticated. /// /// - /// The folder is not currently open in read-write mode. + /// The is not currently open in read-write mode. + /// + /// + /// Cannot store annotations without any properties defined. /// /// - /// The does not support mod-sequences. + /// The does not support annotations. /// /// /// The operation was canceled via the cancellation token. @@ -14502,29 +7709,26 @@ public int Count { /// /// The command failed. /// - public abstract IList RemoveLabels (IList indexes, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + public virtual void Store (UniqueId uid, IList annotations, CancellationToken cancellationToken = default) + { + Store (new [] { uid }, annotations, cancellationToken); + } /// - /// Asynchronously remove a set of labels from the specified messages only if their mod-sequence value is less than the specified value. + /// Asynchronously store the annotations for the specified message. /// /// - /// Asynchronously removes a set of labels from the specified messages only if their mod-sequence value is less than the specified value. + /// Asynchronously stores the annotations for the specified message. /// - /// The indexes of the messages that were not updated. - /// The indexes of the messages. - /// The mod-sequence value. - /// The labels to remove. - /// If set to true, no events will be emitted. + /// An asynchronous task context. + /// The UID of the message. + /// The annotations to store. /// The cancellation token. /// - /// is null. - /// -or- - /// is null. + /// is . /// /// - /// One or more of the is invalid. - /// -or- - /// No labels were specified. + /// is invalid. /// /// /// The has been disposed. @@ -14536,10 +7740,13 @@ public int Count { /// The is not authenticated. /// /// - /// The folder is not currently open in read-write mode. + /// The is not currently open in read-write mode. + /// + /// + /// Cannot store annotations without any properties defined. /// /// - /// The does not support mod-sequences. + /// The does not support annotations. /// /// /// The operation was canceled via the cancellation token. @@ -14553,43 +7760,27 @@ public int Count { /// /// The command failed. /// - public virtual Task> RemoveLabelsAsync (IList indexes, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + public virtual Task StoreAsync (UniqueId uid, IList annotations, CancellationToken cancellationToken = default) { - if (indexes == null) - throw new ArgumentNullException (nameof (indexes)); - - if (labels == null) - throw new ArgumentNullException (nameof (labels)); - - if (labels.Count == 0) - throw new ArgumentException ("No labels were specified.", nameof (labels)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return RemoveLabels (indexes, modseq, labels, silent, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); + return StoreAsync (new [] { uid }, annotations, cancellationToken); } /// - /// Set the labels of the specified messages only if their mod-sequence value is less than the specified value. + /// Store the annotations for the specified messages. /// /// - /// Sets the labels of the specified messages only if their mod-sequence value is less than the specified value. + /// Stores the annotations for the specified messages. /// - /// The indexes of the messages that were not updated. - /// The indexes of the messages. - /// The mod-sequence value. - /// The labels to set. - /// If set to true, no events will be emitted. + /// The UIDs of the messages. + /// The annotations to store. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// - /// One or more of the is invalid. + /// One or more of the is invalid. /// /// /// The has been disposed. @@ -14601,10 +7792,13 @@ public int Count { /// The is not authenticated. /// /// - /// The folder is not currently open in read-write mode. + /// The is not currently open in read-write mode. + /// + /// + /// Cannot store annotations without any properties defined. /// /// - /// The does not support mod-sequences. + /// The does not support annotations. /// /// /// The operation was canceled via the cancellation token. @@ -14618,27 +7812,25 @@ public int Count { /// /// The command failed. /// - public abstract IList SetLabels (IList indexes, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + public abstract void Store (IList uids, IList annotations, CancellationToken cancellationToken = default); /// - /// Asynchronously set the labels of the specified messages only if their mod-sequence value is less than the specified value. + /// Asynchronously store the annotations for the specified messages. /// /// - /// Asynchronously sets the labels of the specified messages only if their mod-sequence value is less than the specified value. + /// Asynchronously stores the annotations for the specified messages. /// - /// The indexes of the messages that were not updated. - /// The indexes of the messages. - /// The mod-sequence value. - /// The labels to set. - /// If set to true, no events will be emitted. + /// An asynchronous task context. + /// The UIDs of the messages. + /// The annotations to store. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// - /// One or more of the is invalid. + /// One or more of the is invalid. /// /// /// The has been disposed. @@ -14650,10 +7842,13 @@ public int Count { /// The is not authenticated. /// /// - /// The folder is not currently open in read-write mode. + /// The is not currently open in read-write mode. + /// + /// + /// Cannot store annotations without any properties defined. /// /// - /// The does not support mod-sequences. + /// The does not support annotations. /// /// /// The operation was canceled via the cancellation token. @@ -14667,36 +7862,26 @@ public int Count { /// /// The command failed. /// - public virtual Task> SetLabelsAsync (IList indexes, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - if (indexes == null) - throw new ArgumentNullException (nameof (indexes)); - - if (labels == null) - throw new ArgumentNullException (nameof (labels)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return SetLabels (indexes, modseq, labels, silent, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task StoreAsync (IList uids, IList annotations, CancellationToken cancellationToken = default); /// - /// Search the folder for messages matching the specified query. + /// Store the annotations for the specified messages only if their mod-sequence value is less than the specified value. /// /// - /// The returned array of unique identifiers can be used with methods such as - /// . + /// Stores the annotations for the specified messages only if their mod-sequence value is less than the specified value. /// - /// An array of matching UIDs. - /// The search query. + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The annotations to store. /// The cancellation token. /// - /// is null. + /// is . + /// -or- + /// is . /// - /// - /// One or more search terms in the are not supported by the mail store. + /// + /// One or more of the is invalid. /// /// /// The has been disposed. @@ -14708,7 +7893,15 @@ public int Count { /// The is not authenticated. /// /// - /// The folder is not currently open. + /// The is not currently open in read-write mode. + /// + /// + /// Cannot store annotations without any properties defined. + /// + /// + /// The does not support annotations. + /// -or- + /// The does not support mod-sequences. /// /// /// The operation was canceled via the cancellation token. @@ -14722,23 +7915,26 @@ public int Count { /// /// The command failed. /// - public abstract IList Search (SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)); + public abstract IList Store (IList uids, ulong modseq, IList annotations, CancellationToken cancellationToken = default); /// - /// Asynchronously search the folder for messages matching the specified query. + /// Asynchronously store the annotations for the specified messages only if their mod-sequence value is less than the specified value. /// /// - /// The returned array of unique identifiers can be used with methods such as - /// . + /// Asynchronously stores the annotations for the specified messages only if their mod-sequence value is less than the specified value. /// - /// An array of matching UIDs. - /// The search query. + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The annotations to store. /// The cancellation token. /// - /// is null. + /// is . + /// -or- + /// is . /// - /// - /// One or more search terms in the are not supported by the mail store. + /// + /// One or more of the is invalid. /// /// /// The has been disposed. @@ -14750,7 +7946,15 @@ public int Count { /// The is not authenticated. /// /// - /// The folder is not currently open. + /// The is not currently open in read-write mode. + /// + /// + /// Cannot store annotations without any properties defined. + /// + /// + /// The does not support annotations. + /// -or- + /// The does not support mod-sequences. /// /// /// The operation was canceled via the cancellation token. @@ -14764,42 +7968,22 @@ public int Count { /// /// The command failed. /// - public virtual Task> SearchAsync (SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)) - { - if (query == null) - throw new ArgumentNullException (nameof (query)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return Search (query, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task> StoreAsync (IList uids, ulong modseq, IList annotations, CancellationToken cancellationToken = default); /// - /// Search the folder for messages matching the specified query, - /// returning them in the preferred sort order. + /// Store the annotations for the specified message. /// /// - /// The returned array of unique identifiers will be sorted in the preferred order and - /// can be used with . + /// Stores the annotations for the specified message. /// - /// An array of matching UIDs in the specified sort order. - /// The search query. - /// The sort order. + /// The index of the message. + /// The annotations to store. /// The cancellation token. /// - /// is null. - /// -or- - /// is null. + /// is . /// /// - /// is empty. - /// - /// - /// One or more search terms in the are not supported. - /// -or- - /// The server does not support sorting search results. + /// is invalid. /// /// /// The has been disposed. @@ -14811,7 +7995,13 @@ public int Count { /// The is not authenticated. /// /// - /// The folder is not currently open. + /// The is not currently open in read-write mode. + /// + /// + /// Cannot store annotations without any properties defined. + /// + /// + /// The does not support annotations. /// /// /// The operation was canceled via the cancellation token. @@ -14825,36 +8015,26 @@ public int Count { /// /// The command failed. /// - [Obsolete ("Use Sort(SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)) instead.")] - public IList Search (SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)) + public virtual void Store (int index, IList annotations, CancellationToken cancellationToken = default) { - return Sort (query, orderBy, cancellationToken); + Store (new[] { index }, annotations, cancellationToken); } /// - /// Asynchronously search the folder for messages matching the specified query, - /// returning them in the preferred sort order. + /// Asynchronously store the annotations for the specified message. /// /// - /// The returned array of unique identifiers will be sorted in the preferred order and - /// can be used with . + /// Asynchronously stores the annotations for the specified message. /// - /// An array of matching UIDs in the specified sort order. - /// The search query. - /// The sort order. + /// An asynchronous task context. + /// The indexes of the message. + /// The annotations to store. /// The cancellation token. /// - /// is null. - /// -or- - /// is null. + /// is . /// /// - /// is empty. - /// - /// - /// One or more search terms in the are not supported. - /// -or- - /// The server does not support sorting search results. + /// is invalid. /// /// /// The has been disposed. @@ -14866,7 +8046,13 @@ public int Count { /// The is not authenticated. /// /// - /// The folder is not currently open. + /// The is not currently open in read-write mode. + /// + /// + /// Cannot store annotations without any properties defined. + /// + /// + /// The does not support annotations. /// /// /// The operation was canceled via the cancellation token. @@ -14880,35 +8066,27 @@ public int Count { /// /// The command failed. /// - [Obsolete ("Use SortAsync(SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)) instead.")] - public virtual Task> SearchAsync (SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)) + public virtual Task StoreAsync (int index, IList annotations, CancellationToken cancellationToken = default) { - return SortAsync (query, orderBy, cancellationToken); + return StoreAsync (new [] { index }, annotations, cancellationToken); } /// - /// Search the subset of UIDs in the folder for messages matching the specified query. + /// Store the annotations for the specified messages. /// /// - /// The returned array of unique identifiers can be used with methods such as - /// . + /// Stores the annotations for the specified messages. /// - /// An array of matching UIDs in the specified sort order. - /// The subset of UIDs - /// The search query. + /// The indexes of the messages. + /// The annotations to store. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// - /// is empty. - /// -or- - /// One or more of the is invalid. - /// - /// - /// One or more search terms in the are not supported by the mail store. + /// One or more of the is invalid. /// /// /// The has been disposed. @@ -14920,7 +8098,13 @@ public int Count { /// The is not authenticated. /// /// - /// The folder is not currently open. + /// The is not currently open in read-write mode. + /// + /// + /// Cannot store annotations without any properties defined. + /// + /// + /// The does not support annotations. /// /// /// The operation was canceled via the cancellation token. @@ -14934,39 +8118,25 @@ public int Count { /// /// The command failed. /// - public virtual IList Search (IList uids, SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)) - { - var uidSet = new UidSearchQuery (uids); - - if (query == null) - throw new ArgumentNullException (nameof (query)); - - return Search (uidSet.And (query), cancellationToken); - } + public abstract void Store (IList indexes, IList annotations, CancellationToken cancellationToken = default); /// - /// Asynchronously search the subset of UIDs in the folder for messages matching the specified query. + /// Asynchronously store the annotations for the specified messages. /// /// - /// The returned array of unique identifiers can be used with methods such as - /// . + /// Asynchronously stores the annotations for the specified messages. /// - /// An array of matching UIDs in the specified sort order. - /// The subset of UIDs - /// The search query. + /// An asynchronous task context. + /// The indexes of the messages. + /// The annotations to store. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// - /// is empty. - /// -or- - /// One or more of the is invalid. - /// - /// - /// One or more search terms in the are not supported. + /// One or more of the is invalid. /// /// /// The has been disposed. @@ -14978,7 +8148,13 @@ public int Count { /// The is not authenticated. /// /// - /// The folder is not currently open. + /// The is not currently open in read-write mode. + /// + /// + /// Cannot store annotations without any properties defined. + /// + /// + /// The does not support annotations. /// /// /// The operation was canceled via the cancellation token. @@ -14992,52 +8168,26 @@ public int Count { /// /// The command failed. /// - public virtual Task> SearchAsync (IList uids, SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)) - { - if (uids == null) - throw new ArgumentNullException (nameof (uids)); - - if (query == null) - throw new ArgumentNullException (nameof (query)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return Search (uids, query, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task StoreAsync (IList indexes, IList annotations, CancellationToken cancellationToken = default); /// - /// Search the subset of UIDs in the folder for messages matching the specified query, - /// returning them in the preferred sort order. + /// Store the annotations for the specified messages only if their mod-sequence value is less than the specified value. /// /// - /// The returned array of unique identifiers will be sorted in the preferred order and - /// can be used with . + /// Stores the annotations for the specified messages only if their mod-sequence value is less than the specified value. /// - /// An array of matching UIDs. - /// The subset of UIDs - /// The search query. - /// The sort order. + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The annotations to store. /// The cancellation token. /// - /// is null. - /// -or- - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// - /// is empty. - /// -or- - /// One or more of the is invalid. - /// -or- - /// is empty. - /// - /// - /// One or more search terms in the are not supported. - /// -or- - /// The server does not support sorting search results. + /// One or more of the is invalid. /// /// /// The has been disposed. @@ -15049,7 +8199,15 @@ public int Count { /// The is not authenticated. /// /// - /// The folder is not currently open. + /// The is not currently open in read-write mode. + /// + /// + /// Cannot store annotations without any properties defined. + /// + /// + /// The does not support annotations. + /// -or- + /// The does not support mod-sequences. /// /// /// The operation was canceled via the cancellation token. @@ -15063,43 +8221,26 @@ public int Count { /// /// The command failed. /// - [Obsolete ("Use Sort(IList uids, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)) instead.")] - public IList Search (IList uids, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)) - { - return Sort (uids, query, orderBy, cancellationToken); - } + public abstract IList Store (IList indexes, ulong modseq, IList annotations, CancellationToken cancellationToken = default); /// - /// Asynchronously search the subset of UIDs in the folder for messages matching the specified query, - /// returning them in the preferred sort order. + /// Asynchronously store the annotations for the specified messages only if their mod-sequence value is less than the specified value. /// /// - /// The returned array of unique identifiers will be sorted in the preferred order and - /// can be used with . + /// Asynchronously stores the annotations for the specified messages only if their mod-sequence value is less than the specified value.s /// - /// An array of matching UIDs. - /// The subset of UIDs - /// The search query. - /// The sort order. + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The annotations to store. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. - /// -or- - /// is null. + /// is . /// /// - /// is empty. - /// -or- - /// One or more of the is invalid. - /// -or- - /// is empty. - /// - /// - /// One or more search terms in the are not supported. - /// -or- - /// The server does not support sorting search results. + /// One or more of the is invalid. /// /// /// The has been disposed. @@ -15111,7 +8252,15 @@ public int Count { /// The is not authenticated. /// /// - /// The folder is not currently open. + /// The is not currently open in read-write mode. + /// + /// + /// Cannot store annotations without any properties defined. + /// + /// + /// The does not support annotations. + /// -or- + /// The does not support mod-sequences. /// /// /// The operation was canceled via the cancellation token. @@ -15125,30 +8274,23 @@ public int Count { /// /// The command failed. /// - [Obsolete ("Use SortAsync(IList uids, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)) instead.")] - public Task> SearchAsync (IList uids, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)) - { - return SortAsync (uids, query, orderBy, cancellationToken); - } + public abstract Task> StoreAsync (IList indexes, ulong modseq, IList annotations, CancellationToken cancellationToken = default); /// /// Search the folder for messages matching the specified query. /// /// - /// Searches the folder for messages matching the specified query, - /// returning only the specified search results. + /// The returned array of unique identifiers can be used with methods such as + /// . /// - /// The search results. - /// The search options. + /// An array of matching UIDs. /// The search query. /// The cancellation token. /// - /// is null. + /// is . /// /// - /// One or more search terms in the are not supported. - /// -or- - /// The server does not support the specified search options. + /// One or more search terms in the are not supported by the mail store. /// /// /// The has been disposed. @@ -15160,7 +8302,7 @@ public int Count { /// The is not authenticated. /// /// - /// The is not currently open. + /// The folder is not currently open. /// /// /// The operation was canceled via the cancellation token. @@ -15174,26 +8316,28 @@ public int Count { /// /// The command failed. /// - public abstract SearchResults Search (SearchOptions options, SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)); + public virtual IList Search (SearchQuery query, CancellationToken cancellationToken = default) + { + var results = Search (SearchOptions.None, query, cancellationToken); + + return results.UniqueIds; + } /// /// Asynchronously search the folder for messages matching the specified query. /// /// - /// Asynchronously searches the folder for messages matching the specified query, - /// returning only the specified search results. + /// The returned array of unique identifiers can be used with methods such as + /// . /// - /// The search results. - /// The search options. + /// An array of matching UIDs. /// The search query. /// The cancellation token. /// - /// is null. + /// is . /// /// - /// One or more search terms in the are not supported. - /// -or- - /// The server does not support the specified search options. + /// One or more search terms in the are not supported by the mail store. /// /// /// The has been disposed. @@ -15205,7 +8349,7 @@ public int Count { /// The is not authenticated. /// /// - /// The is not currently open. + /// The folder is not currently open. /// /// /// The operation was canceled via the cancellation token. @@ -15219,44 +8363,36 @@ public int Count { /// /// The command failed. /// - public virtual Task SearchAsync (SearchOptions options, SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)) + public virtual async Task> SearchAsync (SearchQuery query, CancellationToken cancellationToken = default) { - if (query == null) - throw new ArgumentNullException (nameof (query)); + var results = await SearchAsync (SearchOptions.None, query, cancellationToken).ConfigureAwait (false); - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return Search (options, query, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); + return results.UniqueIds; } /// - /// Sort messages matching the specified query. + /// Search the subset of UIDs in the folder for messages matching the specified query. /// /// - /// Searches the folder for messages matching the specified query, - /// returning the search results in the specified sort order. + /// The returned array of unique identifiers can be used with methods such as + /// . /// - /// The search results. - /// The search options. + /// An array of matching UIDs in the specified sort order. + /// The subset of UIDs /// The search query. - /// The sort order. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// - /// is empty. + /// is empty. + /// -or- + /// One or more of the is invalid. /// /// - /// One or more search terms in the are not supported. - /// -or- - /// The server does not support the specified search options. - /// -or- - /// The server does not support sorting search results. + /// One or more search terms in the are not supported by the mail store. /// /// /// The has been disposed. @@ -15268,7 +8404,7 @@ public int Count { /// The is not authenticated. /// /// - /// The is not currently open. + /// The folder is not currently open. /// /// /// The operation was canceled via the cancellation token. @@ -15282,39 +8418,39 @@ public int Count { /// /// The command failed. /// - [Obsolete ("Use Sort(SearchOptions options, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)) instead.")] - public SearchResults Search (SearchOptions options, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)) + public virtual IList Search (IList uids, SearchQuery query, CancellationToken cancellationToken = default) { - return Sort (options, query, orderBy, cancellationToken); + var uidSet = new UidSearchQuery (uids); + + if (query == null) + throw new ArgumentNullException (nameof (query)); + + return Search (uidSet.And (query), cancellationToken); } /// - /// Asynchronously sort messages matching the specified query, - /// returning them in the preferred sort order. + /// Asynchronously search the subset of UIDs in the folder for messages matching the specified query. /// /// - /// Asynchronously searches the folder for messages matching the specified query, - /// returning the search results in the specified sort order. + /// The returned array of unique identifiers can be used with methods such as + /// . /// - /// The search results. - /// The search options. + /// An array of matching UIDs in the specified sort order. + /// The subset of UIDs /// The search query. - /// The sort order. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// - /// is empty. + /// is empty. + /// -or- + /// One or more of the is invalid. /// /// - /// One or more search terms in the are not supported. - /// -or- - /// The server does not support the specified search options. - /// -or- - /// The server does not support sorting search results. + /// One or more search terms in the are not supported. /// /// /// The has been disposed. @@ -15326,7 +8462,7 @@ public int Count { /// The is not authenticated. /// /// - /// The is not currently open. + /// The folder is not currently open. /// /// /// The operation was canceled via the cancellation token. @@ -15340,33 +8476,29 @@ public int Count { /// /// The command failed. /// - [Obsolete ("Use SortAsync(SearchOptions options, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)) instead.")] - public Task SearchAsync (SearchOptions options, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)) + public virtual Task> SearchAsync (IList uids, SearchQuery query, CancellationToken cancellationToken = default) { - return SortAsync (options, query, orderBy, cancellationToken); + var uidSet = new UidSearchQuery (uids); + + if (query == null) + throw new ArgumentNullException (nameof (query)); + + return SearchAsync (uidSet.And (query), cancellationToken); } /// - /// Searches the subset of UIDs in the folder for messages matching the specified query. + /// Search the folder for messages matching the specified query. /// /// - /// Searches the fsubset of UIDs in the folder for messages matching the specified query, + /// Searches the folder for messages matching the specified query, /// returning only the specified search results. /// /// The search results. /// The search options. - /// The subset of UIDs /// The search query. /// The cancellation token. /// - /// is null. - /// -or- - /// is null. - /// - /// - /// is empty. - /// -or- - /// One or more of the is invalid. + /// is . /// /// /// One or more search terms in the are not supported. @@ -15383,7 +8515,7 @@ public int Count { /// The is not authenticated. /// /// - /// The folder is not currently open. + /// The is not currently open. /// /// /// The operation was canceled via the cancellation token. @@ -15397,37 +8529,21 @@ public int Count { /// /// The command failed. /// - public virtual SearchResults Search (SearchOptions options, IList uids, SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)) - { - var uidSet = new UidSearchQuery (uids); - - if (query == null) - throw new ArgumentNullException (nameof (query)); - - return Search (options, uidSet.And (query), cancellationToken); - } + public abstract SearchResults Search (SearchOptions options, SearchQuery query, CancellationToken cancellationToken = default); /// - /// Asynchronously searches the subset of UIDs in the folder for messages matching the specified query. + /// Asynchronously search the folder for messages matching the specified query. /// /// - /// Asynchronously searches the fsubset of UIDs in the folder for messages matching the specified query, + /// Asynchronously searches the folder for messages matching the specified query, /// returning only the specified search results. /// /// The search results. /// The search options. - /// The subset of UIDs /// The search query. /// The cancellation token. /// - /// is null. - /// -or- - /// is null. - /// - /// - /// is empty. - /// -or- - /// One or more of the is invalid. + /// is . /// /// /// One or more search terms in the are not supported. @@ -15444,7 +8560,7 @@ public int Count { /// The is not authenticated. /// /// - /// The folder is not currently open. + /// The is not currently open. /// /// /// The operation was canceled via the cancellation token. @@ -15458,54 +8574,34 @@ public int Count { /// /// The command failed. /// - public virtual Task SearchAsync (SearchOptions options, IList uids, SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)) - { - if (uids == null) - throw new ArgumentNullException (nameof (uids)); - - if (query == null) - throw new ArgumentNullException (nameof (query)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return Search (options, uids, query, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task SearchAsync (SearchOptions options, SearchQuery query, CancellationToken cancellationToken = default); /// - /// Sort messages matching the specified query. + /// Search the subset of UIDs in the folder for messages matching the specified query. /// /// - /// Searches the folder for messages matching the specified query, - /// returning the search results in the specified sort order. + /// Searches the subset of UIDs in the folder for messages matching the specified query, + /// returning only the specified search results. /// /// The search results. /// The search options. /// The subset of UIDs /// The search query. - /// The sort order. /// The cancellation token. /// - /// is null. - /// -or- - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// is empty. /// -or- /// One or more of the is invalid. - /// -or- - /// is empty. /// /// /// One or more search terms in the are not supported. /// -or- /// The server does not support the specified search options. - /// -or- - /// The server does not support sorting search results. /// /// /// The has been disposed. @@ -15531,45 +8627,42 @@ public int Count { /// /// The command failed. /// - [Obsolete ("Use Sort(SearchOptions options, IList uids, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)) instead.")] - public SearchResults Search (SearchOptions options, IList uids, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)) + public virtual SearchResults Search (SearchOptions options, IList uids, SearchQuery query, CancellationToken cancellationToken = default) { - return Sort (options, uids, query, orderBy, cancellationToken); + var uidSet = new UidSearchQuery (uids); + + if (query == null) + throw new ArgumentNullException (nameof (query)); + + return Search (options, uidSet.And (query), cancellationToken); } /// - /// Asynchronously sort messages matching the specified query. + /// Asynchronously search the subset of UIDs in the folder for messages matching the specified query. /// /// - /// Asynchronously searches the folder for messages matching the specified query, - /// returning the search results in the specified sort order. + /// Asynchronously searches the subset of UIDs in the folder for messages matching the specified query, + /// returning only the specified search results. /// /// The search results. /// The search options. /// The subset of UIDs /// The search query. - /// The sort order. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. - /// -or- - /// is null. + /// is . /// /// /// is empty. /// -or- /// One or more of the is invalid. - /// -or- - /// is empty. /// /// /// One or more search terms in the are not supported. /// -or- /// The server does not support the specified search options. - /// -or- - /// The server does not support sorting search results. /// /// /// The has been disposed. @@ -15595,10 +8688,14 @@ public int Count { /// /// The command failed. /// - [Obsolete ("Use SortAsync(SearchOptions options, IList uids, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)) instead.")] - public Task SearchAsync (SearchOptions options, IList uids, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)) + public virtual Task SearchAsync (SearchOptions options, IList uids, SearchQuery query, CancellationToken cancellationToken = default) { - return SortAsync (options, uids, query, orderBy, cancellationToken); + var uidSet = new UidSearchQuery (uids); + + if (query == null) + throw new ArgumentNullException (nameof (query)); + + return SearchAsync (options, uidSet.And (query), cancellationToken); } /// @@ -15613,9 +8710,9 @@ public int Count { /// The sort order. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// is empty. @@ -15649,7 +8746,12 @@ public int Count { /// /// The command failed. /// - public abstract IList Sort (SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)); + public virtual IList Sort (SearchQuery query, IList orderBy, CancellationToken cancellationToken = default) + { + var results = Sort (SearchOptions.None, query, orderBy, cancellationToken); + + return results.UniqueIds; + } /// /// Asynchronously sort messages matching the specified query. @@ -15663,9 +8765,9 @@ public int Count { /// The sort order. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// is empty. @@ -15699,22 +8801,11 @@ public int Count { /// /// The command failed. /// - public virtual Task> SortAsync (SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)) + public virtual async Task> SortAsync (SearchQuery query, IList orderBy, CancellationToken cancellationToken = default) { - if (query == null) - throw new ArgumentNullException (nameof (query)); - - if (orderBy == null) - throw new ArgumentNullException (nameof (orderBy)); + var results = await SortAsync (SearchOptions.None, query, orderBy, cancellationToken).ConfigureAwait (false); - if (orderBy.Count == 0) - throw new ArgumentException ("No sort order provided.", nameof (orderBy)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return Sort (query, orderBy, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); + return results.UniqueIds; } /// @@ -15730,11 +8821,11 @@ public int Count { /// The sort order. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// is empty. @@ -15772,7 +8863,7 @@ public int Count { /// /// The command failed. /// - public virtual IList Sort (IList uids, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)) + public virtual IList Sort (IList uids, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default) { var uidSet = new UidSearchQuery (uids); @@ -15795,11 +8886,11 @@ public int Count { /// The sort order. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// is empty. @@ -15837,25 +8928,14 @@ public int Count { /// /// The command failed. /// - public virtual Task> SortAsync (IList uids, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)) + public virtual Task> SortAsync (IList uids, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default) { - if (uids == null) - throw new ArgumentNullException (nameof (uids)); + var uidSet = new UidSearchQuery (uids); if (query == null) throw new ArgumentNullException (nameof (query)); - if (orderBy == null) - throw new ArgumentNullException (nameof (orderBy)); - - if (orderBy.Count == 0) - throw new ArgumentException ("No sort order provided.", nameof (orderBy)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return Sort (uids, query, orderBy, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); + return SortAsync (uidSet.And (query), orderBy, cancellationToken); } /// @@ -15870,9 +8950,9 @@ public int Count { /// The sort order. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// is empty. @@ -15908,7 +8988,7 @@ public int Count { /// /// The command failed. /// - public abstract SearchResults Sort (SearchOptions options, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)); + public abstract SearchResults Sort (SearchOptions options, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default); /// /// Asynchronously sort messages matching the specified query. @@ -15922,9 +9002,9 @@ public int Count { /// The sort order. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// is empty. @@ -15960,23 +9040,7 @@ public int Count { /// /// The command failed. /// - public virtual Task SortAsync (SearchOptions options, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)) - { - if (query == null) - throw new ArgumentNullException (nameof (query)); - - if (orderBy == null) - throw new ArgumentNullException (nameof (orderBy)); - - if (orderBy.Count == 0) - throw new ArgumentException ("No sort order provided.", nameof (orderBy)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return Sort (options, query, orderBy, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task SortAsync (SearchOptions options, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default); /// /// Sort messages matching the specified query. @@ -15991,11 +9055,11 @@ public int Count { /// The sort order. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// is empty. @@ -16035,7 +9099,7 @@ public int Count { /// /// The command failed. /// - public virtual SearchResults Sort (SearchOptions options, IList uids, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)) + public virtual SearchResults Sort (SearchOptions options, IList uids, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default) { var uidSet = new UidSearchQuery (uids); @@ -16059,11 +9123,11 @@ public int Count { /// The sort order. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// is empty. @@ -16103,25 +9167,14 @@ public int Count { /// /// The command failed. /// - public virtual Task SortAsync (SearchOptions options, IList uids, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)) + public virtual Task SortAsync (SearchOptions options, IList uids, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default) { - if (uids == null) - throw new ArgumentNullException (nameof (uids)); + var uidSet = new UidSearchQuery (uids); if (query == null) throw new ArgumentNullException (nameof (query)); - if (orderBy == null) - throw new ArgumentNullException (nameof (orderBy)); - - if (orderBy.Count == 0) - throw new ArgumentException ("No sort order provided.", nameof (orderBy)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return Sort (options, uids, query, orderBy, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); + return SortAsync (options, uidSet.And (query), orderBy, cancellationToken); } /// @@ -16139,7 +9192,7 @@ public int Count { /// is not supported. /// /// - /// is null. + /// is . /// /// /// One or more search terms in the are not supported. @@ -16170,7 +9223,7 @@ public int Count { /// /// The command failed. /// - public abstract IList Thread (ThreadingAlgorithm algorithm, SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)); + public abstract IList Thread (ThreadingAlgorithm algorithm, SearchQuery query, CancellationToken cancellationToken = default); /// /// Asynchronously thread the messages in the folder that match the search query using the specified threading algorithm. @@ -16187,7 +9240,7 @@ public int Count { /// is not supported. /// /// - /// is null. + /// is . /// /// /// One or more search terms in the are not supported. @@ -16218,17 +9271,7 @@ public int Count { /// /// The command failed. /// - public virtual Task> ThreadAsync (ThreadingAlgorithm algorithm, SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)) - { - if (query == null) - throw new ArgumentNullException (nameof (query)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return Thread (algorithm, query, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task> ThreadAsync (ThreadingAlgorithm algorithm, SearchQuery query, CancellationToken cancellationToken = default); /// /// Thread the messages in the folder that match the search query using the specified threading algorithm. @@ -16246,9 +9289,9 @@ public int Count { /// is not supported. /// /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// is empty. @@ -16284,7 +9327,7 @@ public int Count { /// /// The command failed. /// - public abstract IList Thread (IList uids, ThreadingAlgorithm algorithm, SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)); + public abstract IList Thread (IList uids, ThreadingAlgorithm algorithm, SearchQuery query, CancellationToken cancellationToken = default); /// /// Asynchronously thread the messages in the folder that match the search query using the specified threading algorithm. @@ -16302,9 +9345,9 @@ public int Count { /// is not supported. /// /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// is empty. @@ -16340,23 +9383,7 @@ public int Count { /// /// The command failed. /// - public virtual Task> ThreadAsync (IList uids, ThreadingAlgorithm algorithm, SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)) - { - if (uids == null) - throw new ArgumentNullException (nameof (uids)); - - if (uids.Count == 0) - throw new ArgumentException ("No uids were specified.", nameof (uids)); - - if (query == null) - throw new ArgumentNullException (nameof (query)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return Thread (uids, algorithm, query, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task> ThreadAsync (IList uids, ThreadingAlgorithm algorithm, SearchQuery query, CancellationToken cancellationToken = default); /// /// Occurs when the folder is opened. @@ -16364,7 +9391,7 @@ public int Count { /// /// The event is emitted when the folder is opened. /// - public event EventHandler Opened; + public event EventHandler? Opened; /// /// Raise the opened event. @@ -16374,10 +9401,7 @@ public int Count { /// protected virtual void OnOpened () { - var handler = Opened; - - if (handler != null) - handler (this, EventArgs.Empty); + Opened?.Invoke (this, EventArgs.Empty); } /// @@ -16386,7 +9410,7 @@ protected virtual void OnOpened () /// /// The event is emitted when the folder is closed. /// - public event EventHandler Closed; + public event EventHandler? Closed; /// /// Raise the closed event. @@ -16396,10 +9420,16 @@ protected virtual void OnOpened () /// internal protected virtual void OnClosed () { - var handler = Closed; + PermanentFlags = MessageFlags.None; + AcceptedFlags = MessageFlags.None; + Access = FolderAccess.None; + FirstUnread = -1; + + AnnotationAccess = AnnotationAccess.None; + AnnotationScopes = AnnotationScope.None; + MaxAnnotationSize = 0; - if (handler != null) - handler (this, EventArgs.Empty); + Closed?.Invoke (this, EventArgs.Empty); } /// @@ -16408,7 +9438,7 @@ internal protected virtual void OnClosed () /// /// The event is emitted when the folder is deleted. /// - public event EventHandler Deleted; + public event EventHandler? Deleted; /// /// Raise the deleted event. @@ -16418,10 +9448,7 @@ internal protected virtual void OnClosed () /// protected virtual void OnDeleted () { - var handler = Deleted; - - if (handler != null) - handler (this, EventArgs.Empty); + Deleted?.Invoke (this, EventArgs.Empty); } /// @@ -16430,7 +9457,7 @@ protected virtual void OnDeleted () /// /// The event is emitted when the folder is renamed. /// - public event EventHandler Renamed; + public event EventHandler? Renamed; /// /// Raise the renamed event. @@ -16442,10 +9469,7 @@ protected virtual void OnDeleted () /// The new name of the folder. protected virtual void OnRenamed (string oldName, string newName) { - var handler = Renamed; - - if (handler != null) - handler (this, new FolderRenamedEventArgs (oldName, newName)); + Renamed?.Invoke (this, new FolderRenamedEventArgs (oldName, newName)); } /// @@ -16460,7 +9484,7 @@ protected virtual void OnParentFolderRenamed () { } - void OnParentFolderRenamed (object sender, FolderRenamedEventArgs e) + void OnParentFolderRenamed (object? sender, FolderRenamedEventArgs e) { var oldFullName = FullName; @@ -16476,7 +9500,7 @@ void OnParentFolderRenamed (object sender, FolderRenamedEventArgs e) /// /// The event is emitted when the folder is subscribed. /// - public event EventHandler Subscribed; + public event EventHandler? Subscribed; /// /// Raise the subscribed event. @@ -16486,10 +9510,7 @@ void OnParentFolderRenamed (object sender, FolderRenamedEventArgs e) /// protected virtual void OnSubscribed () { - var handler = Subscribed; - - if (handler != null) - handler (this, EventArgs.Empty); + Subscribed?.Invoke (this, EventArgs.Empty); } /// @@ -16498,7 +9519,7 @@ protected virtual void OnSubscribed () /// /// The event is emitted when the folder is unsubscribed. /// - public event EventHandler Unsubscribed; + public event EventHandler? Unsubscribed; /// /// Raise the unsubscribed event. @@ -16508,10 +9529,7 @@ protected virtual void OnSubscribed () /// protected virtual void OnUnsubscribed () { - var handler = Unsubscribed; - - if (handler != null) - handler (this, EventArgs.Empty); + Unsubscribed?.Invoke (this, EventArgs.Empty); } /// @@ -16523,7 +9541,7 @@ protected virtual void OnUnsubscribed () /// /// /// - public event EventHandler MessageExpunged; + public event EventHandler? MessageExpunged; /// /// Raise the message expunged event. @@ -16534,33 +9552,7 @@ protected virtual void OnUnsubscribed () /// The message expunged event args. protected virtual void OnMessageExpunged (MessageEventArgs args) { - var handler = MessageExpunged; - - if (handler != null) - handler (this, args); - } - - /// - /// Occurs when new messages arrive in the folder. - /// - /// - /// Emitted when new messages arrive in the folder. - /// - public event EventHandler MessagesArrived; - - /// - /// Raise the messages arrived event. - /// - /// - /// Raises the messages arrived event. - /// - /// The messages arrived event args. - protected virtual void OnMessagesArrived (MessagesArrivedEventArgs args) - { - var handler = MessagesArrived; - - if (handler != null) - handler (this, args); + MessageExpunged?.Invoke (this, args); } /// @@ -16569,7 +9561,7 @@ protected virtual void OnMessagesArrived (MessagesArrivedEventArgs args) /// /// The event is emitted when messages vanish from the folder. /// - public event EventHandler MessagesVanished; + public event EventHandler? MessagesVanished; /// /// Raise the messages vanished event. @@ -16580,10 +9572,7 @@ protected virtual void OnMessagesArrived (MessagesArrivedEventArgs args) /// The messages vanished event args. protected virtual void OnMessagesVanished (MessagesVanishedEventArgs args) { - var handler = MessagesVanished; - - if (handler != null) - handler (this, args); + MessagesVanished?.Invoke (this, args); } /// @@ -16595,7 +9584,7 @@ protected virtual void OnMessagesVanished (MessagesVanishedEventArgs args) /// /// /// - public event EventHandler MessageFlagsChanged; + public event EventHandler? MessageFlagsChanged; /// /// Raise the message flags changed event. @@ -16606,10 +9595,7 @@ protected virtual void OnMessagesVanished (MessagesVanishedEventArgs args) /// The message flags changed event args. protected virtual void OnMessageFlagsChanged (MessageFlagsChangedEventArgs args) { - var handler = MessageFlagsChanged; - - if (handler != null) - handler (this, args); + MessageFlagsChanged?.Invoke (this, args); } /// @@ -16618,7 +9604,7 @@ protected virtual void OnMessageFlagsChanged (MessageFlagsChangedEventArgs args) /// /// The event is emitted when the labels for a message are changed. /// - public event EventHandler MessageLabelsChanged; + public event EventHandler? MessageLabelsChanged; /// /// Raise the message labels changed event. @@ -16629,19 +9615,51 @@ protected virtual void OnMessageFlagsChanged (MessageFlagsChangedEventArgs args) /// The message labels changed event args. protected virtual void OnMessageLabelsChanged (MessageLabelsChangedEventArgs args) { - var handler = MessageLabelsChanged; + MessageLabelsChanged?.Invoke (this, args); + } + + /// + /// Occurs when annotations changed on a message. + /// + /// + /// The event is emitted when the annotations for a message are changed. + /// + public event EventHandler? AnnotationsChanged; - if (handler != null) - handler (this, args); + /// + /// Raise the message annotations changed event. + /// + /// + /// Raises the message annotations changed event. + /// + /// The message annotations changed event args. + protected virtual void OnAnnotationsChanged (AnnotationsChangedEventArgs args) + { + AnnotationsChanged?.Invoke (this, args); } /// /// Occurs when a message summary is fetched from the folder. /// /// - /// Emitted when a message summary is fetched from the folder. + /// Emitted when a message summary is fetched from the folder. + /// When multiple message summaries are being fetched from a remote folder, + /// it is possible that the connection will drop or some other exception will + /// occur, causing the Fetch method to fail and lose all of the data that has been + /// downloaded up to that point, requiring the client to request the same set of + /// message summaries all over again after it reconnects. This is obviously + /// inefficient. To alleviate this potential problem, this event will be emitted + /// as soon as the successfully parses each untagged FETCH + /// response from the server, allowing the client to commit this data immediately to + /// its local cache. + /// Depending on the IMAP server, it is possible that the + /// event will be emitted for the same message + /// multiple times if the IMAP server happens to split the requested fields into + /// multiple untagged FETCH responses. Use the + /// property to determine which f properties have + /// been populated. /// - public event EventHandler MessageSummaryFetched; + public event EventHandler? MessageSummaryFetched; /// /// Raise the message summary fetched event. @@ -16650,24 +9668,44 @@ protected virtual void OnMessageLabelsChanged (MessageLabelsChangedEventArgs arg /// Raises the message summary fetched event. /// When multiple message summaries are being fetched from a remote folder, /// it is possible that the connection will drop or some other exception will - /// occur, causing the Fetch method to fail, requiring the client to request the - /// same set of message summaries again after it reconnects. This is obviously + /// occur, causing the Fetch method to fail and lose all of the data that has been + /// downloaded up to that point, requiring the client to request the same set of + /// message summaries all over again after it reconnects. This is obviously /// inefficient. To alleviate this potential problem, this event will be emitted - /// as soon as the successfully retrieves the complete - /// for each requested message. - /// The Fetch - /// methods will return a list of all message summaries that any information was - /// retrieved for, regardless of whether or not all of the requested items were fetched, - /// therefore there may be a discrepency between the number of times this event is - /// emitetd and the number of summary items returned from the Fetch method. + /// as soon as the successfully parses each untagged FETCH + /// response from the server, allowing the client to commit this data immediately to + /// its local cache. + /// Depending on the IMAP server, it is possible that + /// will be invoked for the same message + /// multiple times if the IMAP server happens to split the requested fields into + /// multiple untagged FETCH responses. Use the + /// property to determine which f properties have + /// been populated. /// /// The message summary. protected virtual void OnMessageSummaryFetched (IMessageSummary message) { - var handler = MessageSummaryFetched; + MessageSummaryFetched?.Invoke (this, new MessageSummaryFetchedEventArgs (message)); + } + + /// + /// Occurs when metadata changes. + /// + /// + /// The event is emitted when metadata changes. + /// + public event EventHandler? MetadataChanged; - if (handler != null) - handler (this, new MessageSummaryFetchedEventArgs (message)); + /// + /// Raise the metadata changed event. + /// + /// + /// Raises the metadata changed event. + /// + /// The metadata that changed. + internal protected virtual void OnMetadataChanged (Metadata metadata) + { + MetadataChanged?.Invoke (this, new MetadataChangedEventArgs (metadata)); } /// @@ -16676,7 +9714,7 @@ protected virtual void OnMessageSummaryFetched (IMessageSummary message) /// /// The event is emitted when the mod-sequence for a message is changed. /// - public event EventHandler ModSeqChanged; + public event EventHandler? ModSeqChanged; /// /// Raise the message mod-sequence changed event. @@ -16687,10 +9725,7 @@ protected virtual void OnMessageSummaryFetched (IMessageSummary message) /// The mod-sequence changed event args. protected virtual void OnModSeqChanged (ModSeqChangedEventArgs args) { - var handler = ModSeqChanged; - - if (handler != null) - handler (this, args); + ModSeqChanged?.Invoke (this, args); } /// @@ -16699,7 +9734,7 @@ protected virtual void OnModSeqChanged (ModSeqChangedEventArgs args) /// /// The event is emitted whenever the value changes. /// - public event EventHandler HighestModSeqChanged; + public event EventHandler? HighestModSeqChanged; /// /// Raise the highest mod-sequence changed event. @@ -16709,10 +9744,26 @@ protected virtual void OnModSeqChanged (ModSeqChangedEventArgs args) /// protected virtual void OnHighestModSeqChanged () { - var handler = HighestModSeqChanged; + HighestModSeqChanged?.Invoke (this, EventArgs.Empty); + } + + /// + /// Occurs when the next UID changes. + /// + /// + /// The event is emitted whenever the value changes. + /// + public event EventHandler? UidNextChanged; - if (handler != null) - handler (this, EventArgs.Empty); + /// + /// Raise the next UID changed event. + /// + /// + /// Raises the next UID changed event. + /// + protected virtual void OnUidNextChanged () + { + UidNextChanged?.Invoke (this, EventArgs.Empty); } /// @@ -16721,7 +9772,7 @@ protected virtual void OnHighestModSeqChanged () /// /// The event is emitted whenever the value changes. /// - public event EventHandler UidValidityChanged; + public event EventHandler? UidValidityChanged; /// /// Raise the uid validity changed event. @@ -16731,10 +9782,45 @@ protected virtual void OnHighestModSeqChanged () /// protected virtual void OnUidValidityChanged () { - var handler = UidValidityChanged; + UidValidityChanged?.Invoke (this, EventArgs.Empty); + } + + /// + /// Occurs when the folder ID changes. + /// + /// + /// The event is emitted whenever the value changes. + /// + public event EventHandler? IdChanged; + + /// + /// Raise the ID changed event. + /// + /// + /// Raises the ID changed event. + /// + protected virtual void OnIdChanged () + { + IdChanged?.Invoke (this, EventArgs.Empty); + } + + /// + /// Occurs when the folder size changes. + /// + /// + /// The event is emitted whenever the value changes. + /// + public event EventHandler? SizeChanged; - if (handler != null) - handler (this, EventArgs.Empty); + /// + /// Raise the size changed event. + /// + /// + /// Raises the size changed event. + /// + protected virtual void OnSizeChanged () + { + SizeChanged?.Invoke (this, EventArgs.Empty); } /// @@ -16746,20 +9832,17 @@ protected virtual void OnUidValidityChanged () /// /// /// - public event EventHandler CountChanged; + public event EventHandler? CountChanged; /// - /// Raise the count changed event. + /// Raise the message count changed event. /// /// - /// Raises the count changed event. + /// Raises the message count changed event. /// protected virtual void OnCountChanged () { - var handler = CountChanged; - - if (handler != null) - handler (this, EventArgs.Empty); + CountChanged?.Invoke (this, EventArgs.Empty); } /// @@ -16768,20 +9851,36 @@ protected virtual void OnCountChanged () /// /// The event is emitted whenever the value changes. /// - public event EventHandler RecentChanged; + public event EventHandler? RecentChanged; /// - /// Raise the recent changed event. + /// Raise the recent message count changed event. /// /// - /// Raises the recent changed event. + /// Raises the recent message count changed event. /// protected virtual void OnRecentChanged () { - var handler = RecentChanged; + RecentChanged?.Invoke (this, EventArgs.Empty); + } + + /// + /// Occurs when the unread message count changes. + /// + /// + /// The event is emitted whenever the value changes. + /// + public event EventHandler? UnreadChanged; - if (handler != null) - handler (this, EventArgs.Empty); + /// + /// Raise the unread message count changed event. + /// + /// + /// Raises the unread message count changed event. + /// + protected virtual void OnUnreadChanged () + { + UnreadChanged?.Invoke (this, EventArgs.Empty); } #region IEnumerable implementation diff --git a/MailKit/MailKit.Android.csproj b/MailKit/MailKit.Android.csproj deleted file mode 100644 index 4356b3ac1a..0000000000 --- a/MailKit/MailKit.Android.csproj +++ /dev/null @@ -1,223 +0,0 @@ - - - - Debug - AnyCPU - {9BF818C3-C20E-4EFB-9426-F0A6C2EDC627} - {EFBA0AD7-5A72-4C68-AF49-83D382785DCF};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - Library - MailKit - Resources\Resource.designer.cs - Resource - Resources - Assets - False - MailKit - v4.0.3 - - - true - full - false - bin\Debug\MonoAndroid - obj\Debug\MonoAndroid - DEBUG;TRACE;SERIALIZABLE;__MOBILE__;__ANDROID__; - prompt - 4 - None - false - true - - - true - bin\Release\MonoAndroid - obj\Release\MonoAndroid - SERIALIZABLE;__MOBILE__;__ANDROID__; - prompt - 4 - false - false - true - bin\Release\MonoAndroid\MailKit.xml - - - true - - - mailkit.snk - - - - - - - - - - {004B4019-62B7-4A15-AF2C-C20968845C46} - MimeKit.Android - - - {A0D302CB-8866-4AB1-98B9-F0772EABF5DF} - BouncyCastle.Android - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/MailKit/MailKit.Android.project.json b/MailKit/MailKit.Android.project.json deleted file mode 100644 index 8181ffd1c2..0000000000 --- a/MailKit/MailKit.Android.project.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "frameworks": { - "MonoAndroid,Version=v4.0.3": {} - }, - "runtimes": { - "win-anycpu": {} - } -} \ No newline at end of file diff --git a/MailKit/MailKit.Net40.csproj b/MailKit/MailKit.Net40.csproj deleted file mode 100644 index 61bed2d0c3..0000000000 --- a/MailKit/MailKit.Net40.csproj +++ /dev/null @@ -1,226 +0,0 @@ - - - - Debug - AnyCPU - 10.0.0 - 2.0 - {DB3A2478-4742-452B-80C1-F672B64285AD} - Library - MailKit - MailKit - - - true - full - false - bin\Debug\net40 - obj\Debug\net40 - DEBUG;TRACE;SERIALIZABLE;NET_4_0 - prompt - 4 - false - true - - - true - bin\Release\net40 - obj\Release\net40 - prompt - 4 - false - true - bin\Release\net40\MailKit.xml - SERIALIZABLE;NET_4_0 - - - true - - - mailkit.snk - - - - - - - ..\packages\BouncyCastle.1.8.1\lib\BouncyCastle.Crypto.dll - - - - - {C909FC86-6084-41E5-B99C-DCDF2A5B7F82} - MimeKit.Net40 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/MailKit/MailKit.Net40.project.json b/MailKit/MailKit.Net40.project.json deleted file mode 100644 index 8252854d02..0000000000 --- a/MailKit/MailKit.Net40.project.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "frameworks": { - "net40": {} - }, - "runtimes": { - "win-anycpu": {}, - "win": {} - } -} diff --git a/MailKit/MailKit.Net45.csproj b/MailKit/MailKit.Net45.csproj deleted file mode 100644 index 45ea0d1233..0000000000 --- a/MailKit/MailKit.Net45.csproj +++ /dev/null @@ -1,218 +0,0 @@ - - - - Debug - AnyCPU - AnyCPU - {7264D469-A390-4C10-9C87-DAA37EDD3C1D} - v4.5 - Library - MailKit - MailKit - - - true - full - false - bin\Debug\net45 - obj\Debug\net45 - DEBUG;TRACE;SERIALIZABLE;NET_4_5 - prompt - 4 - false - true - - - true - bin\Release\net45 - obj\Release\net45 - prompt - 4 - false - true - bin\Release\net45\MailKit.xml - SERIALIZABLE;NET_4_5 - - - true - - - mailkit.snk - - - - - - - ..\packages\BouncyCastle.1.8.1\lib\BouncyCastle.Crypto.dll - - - - - {D5F54A4F-D84B-430F-9271-F7861E285B3E} - MimeKit.Net45 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/MailKit/MailKit.Net45.project.json b/MailKit/MailKit.Net45.project.json deleted file mode 100644 index 7b2e820fd1..0000000000 --- a/MailKit/MailKit.Net45.project.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "frameworks": { - "net45": {} - }, - "runtimes": { - "win-anycpu": {}, - "win": {} - } -} diff --git a/MailKit/MailKit.Windows81.csproj b/MailKit/MailKit.Windows81.csproj deleted file mode 100644 index e9d1d669a1..0000000000 --- a/MailKit/MailKit.Windows81.csproj +++ /dev/null @@ -1,309 +0,0 @@ - - - - - Debug - AnyCPU - 8.0.30703 - 2.0 - {D0DC75FD-4DF3-4DDD-BB3B-EDFE2A72EEBF} - Library - Properties - MailKit - MailKit - en-US - 8.1 - 12 - 512 - {BC8A1FFA-BEE3-4634-8014-F334798102B3};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - - - true - full - false - bin\Debug\win81\ - obj\Debug\win81\ - DEBUG;TRACE;NETFX_CORE;WINDOWS_APP - prompt - 4 - true - - - pdbonly - true - bin\Release\win81\ - obj\Release\win81\ - bin\Release\win81\MailKit.xml - TRACE;NETFX_CORE;WINDOWS_APP - prompt - 4 - true - - - true - bin\Debug\win81\arm\ - obj\Debug\win81\arm\ - DEBUG;TRACE;NETFX_CORE;WINDOWS_APP - ;2008 - full - ARM - false - prompt - true - - - bin\Release\win81\arm\ - obj\Release\win81\arm\ - bin\Release\win81\arm\MailKit.xml - TRACE;NETFX_CORE;WINDOWS_APP - true - ;2008 - pdbonly - ARM - false - prompt - true - - - true - bin\Debug\win81\amd64\ - obj\Debug\win81\amd64\ - DEBUG;TRACE;NETFX_CORE;WINDOWS_APP - ;2008 - full - x64 - false - prompt - true - - - bin\Release\win81\amd64\ - obj\Release\win81\amd64\ - bin\Release\win81\amd64\MailKit.xml - TRACE;NETFX_CORE;WINDOWS_APP - true - ;2008 - pdbonly - x64 - false - prompt - true - - - true - bin\Debug\win81\x86\ - obj\Debug\win81\x86\ - DEBUG;TRACE;NETFX_CORE;WINDOWS_APP - ;2008 - full - x86 - false - prompt - true - - - bin\Release\win81\x86\ - obj\Release\win81\x86\ - bin\Release\win81\x86\MailKit.xml - TRACE;NETFX_CORE;WINDOWS_APP - true - ;2008 - pdbonly - x86 - false - prompt - true - - - true - - - mailkit.snk - - - - - - 12.0 - - - - {d9906b8c-7bbd-4cce-ac7c-e9bca020d20c} - MimeKit.WindowsUniversal81 - - - {b76a64f9-b00e-4243-ae89-5d024ca3b436} - Portable.Text.Encoding.WindowsUniversal81 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/MailKit/MailKit.WindowsPhone81.csproj b/MailKit/MailKit.WindowsPhone81.csproj deleted file mode 100644 index 5c04c9fb2f..0000000000 --- a/MailKit/MailKit.WindowsPhone81.csproj +++ /dev/null @@ -1,287 +0,0 @@ - - - - - Debug - AnyCPU - 8.0.30703 - 2.0 - {17E39AA2-C817-4642-BE6B-BC96A657AC53} - Library - Properties - MailKit - MailKit - en-US - 8.1 - 12 - 512 - {76F1466A-8B6D-4E39-A767-685A06062A39};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - - - true - full - false - bin\Debug\wp81\ - obj\Debug\wp81\ - DEBUG;TRACE;NETFX_CORE;WINDOWS_PHONE_APP - prompt - 4 - true - - - pdbonly - true - bin\Release\wp81\ - obj\Release\wp81\ - bin\Release\wp81\MailKit.xml - TRACE;NETFX_CORE;WINDOWS_PHONE_APP - prompt - 4 - true - - - true - bin\Debug\wp81\arm\ - obj\Debug\wp81\arm\ - DEBUG;TRACE;NETFX_CORE;WINDOWS_PHONE_APP - ;2008 - full - ARM - false - prompt - true - - - bin\Release\wp81\arm\ - obj\Release\wp81\arm - bin\Release\wp81\arm\MailKit.xml - TRACE;NETFX_CORE;WINDOWS_PHONE_APP - true - ;2008 - pdbonly - ARM - false - prompt - true - - - true - bin\Debug\wp81\x86\ - obj\Debug\wp81\x86\ - DEBUG;TRACE;NETFX_CORE;WINDOWS_PHONE_APP - ;2008 - full - x86 - false - prompt - true - - - bin\Release\wp81\x86\ - obj\Release\wp81\x86\ - bin\Release\wp81\x86\MailKit.xml - TRACE;NETFX_CORE;WINDOWS_PHONE_APP - true - ;2008 - pdbonly - x86 - false - prompt - true - - - true - - - mailkit.snk - - - - - - 12.0 - - - WindowsPhoneApp - - - - {d9906b8c-7bbd-4cce-ac7c-e9bca020d20c} - MimeKit.WindowsUniversal81 - - - {b76a64f9-b00e-4243-ae89-5d024ca3b436} - Portable.Text.Encoding.WindowsUniversal81 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/MailKit/MailKit.WindowsUniversal81.csproj b/MailKit/MailKit.WindowsUniversal81.csproj deleted file mode 100644 index 408052d98b..0000000000 --- a/MailKit/MailKit.WindowsUniversal81.csproj +++ /dev/null @@ -1,232 +0,0 @@ - - - - - 12.0 - Debug - AnyCPU - {5C20EB98-8084-41E7-952A-F297C0AAC916} - Library - Properties - MailKit - MailKit - v4.6 - Profile32 - en-US - 512 - {786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - - - true - full - false - bin\Debug\wpa81\ - obj\Debug\wpa81\ - TRACE;DEBUG;NETFX_CORE;WINDOWS_APP;WINDOWS_PHONE_APP - prompt - 4 - true - - - pdbonly - true - bin\Release\wpa81\ - obj\Release\wpa81\ - bin\Release\wpa81\MailKit.xml - TRACE;NETFX_CORE;WINDOWS_APP;WINDOWS_PHONE_APP - prompt - 4 - true - - - true - - - mailkit.snk - - - - - - - - {d9906b8c-7bbd-4cce-ac7c-e9bca020d20c} - MimeKit.WindowsUniversal81 - - - {b76a64f9-b00e-4243-ae89-5d024ca3b436} - Portable.Text.Encoding.WindowsUniversal81 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/MailKit/MailKit.WindowsUniversal81.project.json b/MailKit/MailKit.WindowsUniversal81.project.json deleted file mode 100644 index 3667fff5fe..0000000000 --- a/MailKit/MailKit.WindowsUniversal81.project.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "frameworks": { - ".NETPortable,Version=v4.6,Profile=Profile32": {} - }, - "runtimes": { - "win-anycpu": {} - }, - "dependencies": { - "Portable.BouncyCastle": "1.8.1" - } -} \ No newline at end of file diff --git a/MailKit/MailKit.NetStandard.csproj b/MailKit/MailKit.csproj similarity index 56% rename from MailKit/MailKit.NetStandard.csproj rename to MailKit/MailKit.csproj index a64516139d..595f0ed4cb 100644 --- a/MailKit/MailKit.NetStandard.csproj +++ b/MailKit/MailKit.csproj @@ -3,14 +3,15 @@ An Open Source cross-platform .NET mail-client library that is based on MimeKit and optimized for mobile devices. MailKit - 1.16.1 + 4.17.0 Jeffrey Stedfast - netstandard1.3 + 12 + netstandard2.0;netstandard2.1;net462;net47;net48;net8.0;net10.0 true false MailKit MailKit - smtp;pop3;imap;mime;security;dkim;smime;s/mime;openpgp;pgp;mbox;mail;email;parser;tnef;xamarin;android;ios;monodroid;monotouch;net40;net45;wpa81 + smtp;pop3;imap;mime;security;dkim;smime;s/mime;openpgp;pgp;mbox;mail;email;parser;tnef https://github.com/jstedfast/MailKit https://github.com/jstedfast/MailKit/blob/master/License.md false @@ -22,53 +23,87 @@ false false false - - - - $(DefineConstants);NETSTANDARD + MailKit + enable true mailkit.snk + true + true true - true + true + 1701;1702;CA1068;CA1510;CA1512;CA1513;CA1835;CA2012;IDE0016;IDE0056;IDE0057;IDE0060;IDE0063;IDE0066;IDE0090;IDE0180;IDE0251 - - - - - - - - - - + + full + + + + portable + + + + full + + + + + $(DefineConstants);SERIALIZABLE + + + + true + + + + - + - + + + + + + + + + + + + + + + + + + + + + @@ -77,15 +112,37 @@ + + + + + + + + + + + + + + + + + + + + + + @@ -93,6 +150,7 @@ + @@ -103,37 +161,52 @@ - - - - + + - - + + + + - - - - - + + + + + + + + + + + + + + + + + + + + @@ -142,26 +215,44 @@ + + + + + + + + + + + + + + + + + + @@ -173,7 +264,6 @@ - @@ -183,24 +273,33 @@ + + + + + + + + + diff --git a/MailKit/MailKit.iOS.csproj b/MailKit/MailKit.iOS.csproj deleted file mode 100644 index 30c7e52d4a..0000000000 --- a/MailKit/MailKit.iOS.csproj +++ /dev/null @@ -1,218 +0,0 @@ - - - - Debug - AnyCPU - {60B5D72B-8219-48B6-B688-AD0FE284A96A} - {FEACFBD2-3405-455C-9665-78FE426C6842};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - Library - MailKit - Resources - MailKit - Xamarin.iOS - - - true - full - false - bin\Debug\Xamarin.iOS - obj\Debug\Xamarin.iOS - DEBUG;TRACE;SERIALIZABLE;__MOBILE__;__IOS__; - prompt - 4 - false - true - - - true - bin\Release\Xamarin.iOS - obj\Release\Xamarin.iOS - prompt - 4 - false - SERIALIZABLE;__MOBILE__;__IOS__; - true - bin\Release\Xamarin.iOS\MailKit.xml - - - true - - - mailkit.snk - - - - - - - - - - {4C1288AD-12C8-4BF7-AED7-6C4DC539C856} - MimeKit.iOS - - - {0249241C-205E-4AC0-828B-90F822359B9E} - BouncyCastle.iOS - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/MailKit/MailKit.iOS.project.json b/MailKit/MailKit.iOS.project.json deleted file mode 100644 index 4a7c8a5100..0000000000 --- a/MailKit/MailKit.iOS.project.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "frameworks": { - "Xamarin.iOS,Version=v1.0": {} - }, - "runtimes": { - "win-anycpu": {} - } -} \ No newline at end of file diff --git a/MailKit/MailKitLite.csproj b/MailKit/MailKitLite.csproj new file mode 100644 index 0000000000..d54644b764 --- /dev/null +++ b/MailKit/MailKitLite.csproj @@ -0,0 +1,304 @@ + + + + An Open Source cross-platform .NET mail-client library that is based on MimeKit and optimized for mobile devices. + MailKit + 4.17.0 + Jeffrey Stedfast + 10 + netstandard2.0;netstandard2.1;net462;net47;net48;net8.0;net10.0 + true + false + MailKitLite + MailKitLite + smtp;pop3;imap;mime;security;dkim;smime;s/mime;openpgp;pgp;mbox;mail;email;parser;tnef + https://github.com/jstedfast/MailKit + https://github.com/jstedfast/MailKit/blob/master/License.md + false + false + false + false + false + false + false + false + false + MailKit + enable + true + mailkit.snk + true + true + $(DefineConstants);MAILKIT_LITE + true + true + 1701;1702;CA1068;CA1510;CA1512;CA1513;CA1835;CA2012;IDE0016;IDE0056;IDE0057;IDE0060;IDE0063;IDE0066;IDE0090;IDE0180;IDE0251 + + + + full + + + + portable + + + + full + + + + + $(DefineConstants);SERIALIZABLE + + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MailKit/MailService.cs b/MailKit/MailService.cs index a950005920..d81cb5be24 100644 --- a/MailKit/MailService.cs +++ b/MailKit/MailService.cs @@ -1,9 +1,9 @@ -// +// // MailService.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -25,22 +25,23 @@ // using System; +using System.IO; using System.Net; using System.Text; using System.Threading; +using System.Net.Sockets; +using System.Net.Security; using System.Threading.Tasks; using System.Collections.Generic; - -#if !NETFX_CORE -using System.Net.Security; +using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; -using SslProtocols = System.Security.Authentication.SslProtocols; -#else -using Encoding = Portable.Text.Encoding; -#endif +using MailKit.Net; +using MailKit.Net.Proxy; using MailKit.Security; +using NetworkStream = MailKit.Net.NetworkStream; + namespace MailKit { /// /// An abstract mail service implementation. @@ -50,12 +51,6 @@ namespace MailKit { /// public abstract class MailService : IMailService { -#if NET_4_5 || __MOBILE__ - const SslProtocols DefaultSslProtocols = SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12; -#elif !NETFX_CORE - const SslProtocols DefaultSslProtocols = SslProtocols.Tls; -#endif - /// /// Initializes a new instance of the class. /// @@ -64,16 +59,24 @@ public abstract class MailService : IMailService /// /// The protocol logger. /// - /// is null. + /// is . /// protected MailService (IProtocolLogger protocolLogger) { if (protocolLogger == null) throw new ArgumentNullException (nameof (protocolLogger)); -#if !NETFX_CORE - SslProtocols = DefaultSslProtocols; +#if NETFRAMEWORK + // Default the SslProtocols value to whatever the system default is (as defined by ServicePointManager.SecurityProtocol). + // + // See discussion in https://github.com/jstedfast/MailKit/issues/1952 for details. + SslProtocols = (SslProtocols) ServicePointManager.SecurityProtocol; +#else + // Default the SslProtocols value to `None` which allows the operating system to choose the best + // protocol to use and to block protocols that are not secure. + SslProtocols = SslProtocols.None; #endif + CheckCertificateRevocation = true; ProtocolLogger = protocolLogger; } @@ -101,12 +104,10 @@ protected MailService () : this (new NullProtocolLogger ()) } /// - /// Gets an object that can be used to synchronize access to the service. + /// Get an object that can be used to synchronize access to the service. /// /// /// Gets an object that can be used to synchronize access to the service. - /// When using the non-Async methods from multiple threads, it is important to lock the - /// object for thread safety when using the synchronous methods. /// /// The sync root. public abstract object SyncRoot { @@ -114,7 +115,7 @@ public abstract object SyncRoot { } /// - /// Gets the protocol supported by the message service. + /// Get the protocol supported by the message service. /// /// /// Gets the protocol supported by the message service. @@ -135,47 +136,100 @@ public IProtocolLogger ProtocolLogger { get; private set; } -#if !NETFX_CORE /// - /// Gets or sets the SSL/TLS protocols that the client is allowed to use. + /// Get or set the set of enabled SSL and/or TLS protocol versions that the client is allowed to use. /// /// - /// Gets or sets the SSL/TLS protocols that the client is allowed to use. - /// This property should be set before calling any of the - /// Connect methods. + /// Gets or sets the enabled SSL and/or TLS protocol versions that the client is allowed to use. + /// By default, MailKit initializes this value to which allows the + /// operating system to choose the best protocol to use and to block protocols that are not secure. + /// This property should be set before calling any of the + /// Connect or + /// ConnectAsync methods. /// - /// The ssl protocols. + /// The SSL and TLS protocol versions that are enabled. public SslProtocols SslProtocols { get; set; } +#if NET5_0_OR_GREATER /// - /// Gets or sets the client SSL certificates. + /// Get or set the cipher suites allowed to be used when negotiating an SSL or TLS connection. + /// + /// + /// Specifies the cipher suites allowed to be used when negotiating an SSL or TLS connection. + /// When set to , the operating system default is used. Use extreme caution when + /// changing this setting. + /// This property should be set before calling any of the + /// Connect or + /// ConnectAsync methods. + /// + /// The cipher algorithms allowed for use when negotiating SSL or TLS encryption. + public CipherSuitesPolicy? SslCipherSuitesPolicy { + get; set; + } + + /// + /// Get the negotiated SSL or TLS cipher suite. + /// + /// + /// Gets the negotiated SSL or TLS cipher suite once an SSL or TLS connection has been made. + /// + /// The negotiated SSL or TLS cipher suite. + public abstract TlsCipherSuite? SslCipherSuite { + get; + } +#endif + + /// + /// Get or set the client SSL certificates. /// /// /// Some servers may require the client SSL certificates in order /// to allow the user to connect. - /// This property should be set before calling any of the - /// Connect methods. + /// This property should be set before calling any of the + /// Connect or + /// ConnectAsync methods. /// /// The client SSL certificates. - public X509CertificateCollection ClientCertificates { + public X509CertificateCollection? ClientCertificates { + get; set; + } + + /// + /// Get or set whether connecting via SSL/TLS should check certificate revocation. + /// + /// + /// Gets or sets whether connecting via SSL/TLS should check certificate revocation. + /// Normally, the value of this property should be set to (the default) for security + /// reasons, but there are times when it may be necessary to set it to . + /// For example, most Certificate Authorities are probably pretty good at keeping their CRL and/or + /// OCSP servers up 24/7, but occasionally they do go down or are otherwise unreachable due to other + /// network problems between the client and the Certificate Authority. When this happens, it becomes + /// impossible to check the revocation status of one or more of the certificates in the chain + /// resulting in an being thrown in the + /// Connect method. If this becomes a problem, + /// it may become desirable to set to . + /// + /// if certificate revocation should be checked; otherwise, . + public bool CheckCertificateRevocation { get; set; } /// - /// Get or sets a callback function to validate the server certificate. + /// Get or set a callback function to validate the server certificate. /// /// /// Gets or sets a callback function to validate the server certificate. - /// This property should be set before calling any of the - /// Connect methods. + /// This property should be set before calling any of the + /// Connect or + /// ConnectAsync methods. /// /// - /// + /// /// /// The server certificate validation callback function. - public RemoteCertificateValidationCallback ServerCertificateValidationCallback { + public RemoteCertificateValidationCallback? ServerCertificateValidationCallback { get; set; } @@ -185,11 +239,25 @@ public RemoteCertificateValidationCallback ServerCertificateValidationCallback { /// /// Gets or sets the local IP end point to use when connecting to the remote host. /// - /// The local IP end point or null to use the default end point. - public IPEndPoint LocalEndPoint { + /// The local IP end point or to use the default end point. + public IPEndPoint? LocalEndPoint { + get; set; + } + + /// + /// Get or set the proxy client to use when connecting to a remote host. + /// + /// + /// Gets or sets the proxy client to use when connecting to a remote host via any of the + /// Connect methods. + /// + /// + /// + /// + /// The proxy client. + public IProxyClient? ProxyClient { get; set; } -#endif /// /// Gets the authentication mechanisms supported by the mail server. @@ -207,16 +275,16 @@ public abstract HashSet AuthenticationMechanisms { /// Gets whether or not the client is currently connected to an mail server. /// /// - ///The state is set to true immediately after + ///The state is set to immediately after /// one of the Connect - /// methods succeeds and is not set back to false until either the client + /// methods succeeds and is not set back to until either the client /// is disconnected via or until a /// is thrown while attempting to read or write to /// the underlying network socket. /// When an is caught, the connection state of the /// should be checked before continuing. /// - /// true if the client is connected; otherwise, false. + /// if the client is connected; otherwise, . public abstract bool IsConnected { get; } @@ -227,11 +295,128 @@ public abstract bool IsConnected { /// /// Gets whether or not the connection is secure (typically via SSL or TLS). /// - /// true if the connection is secure; otherwise, false. + /// if the connection is secure; otherwise, . public abstract bool IsSecure { get; } + /// + /// Get whether or not the connection is encrypted (typically via SSL or TLS). + /// + /// + /// Gets whether or not the connection is encrypted (typically via SSL or TLS). + /// + /// if the connection is encrypted; otherwise, . + public abstract bool IsEncrypted { + get; + } + + /// + /// Get whether or not the connection is signed (typically via SSL or TLS). + /// + /// + /// Gets whether or not the connection is signed (typically via SSL or TLS). + /// + /// if the connection is signed; otherwise, . + public abstract bool IsSigned { + get; + } + + /// + /// Get the negotiated SSL or TLS protocol version. + /// + /// + /// Gets the negotiated SSL or TLS protocol version once an SSL or TLS connection has been made. + /// + /// The negotiated SSL or TLS protocol version. + public abstract SslProtocols SslProtocol { + get; + } + + /// + /// Get the negotiated SSL or TLS cipher algorithm. + /// + /// + /// Gets the negotiated SSL or TLS cipher algorithm once an SSL or TLS connection has been made. + /// + /// The negotiated SSL or TLS cipher algorithm. +#if NET10_0_OR_GREATER + [Obsolete ("Use SslCipherSuite instead.")] +#endif + public abstract CipherAlgorithmType? SslCipherAlgorithm { + get; + } + + /// + /// Get the negotiated SSL or TLS cipher algorithm strength. + /// + /// + /// Gets the negotiated SSL or TLS cipher algorithm strength once an SSL or TLS connection has been made. + /// + /// The negotiated SSL or TLS cipher algorithm strength. +#if NET10_0_OR_GREATER + [Obsolete ("Use SslCipherSuite instead.")] +#endif + public abstract int? SslCipherStrength { + get; + } + + /// + /// Get the negotiated SSL or TLS hash algorithm. + /// + /// + /// Gets the negotiated SSL or TLS hash algorithm once an SSL or TLS connection has been made. + /// + /// The negotiated SSL or TLS hash algorithm. +#if NET10_0_OR_GREATER + [Obsolete ("Use SslCipherSuite instead.")] +#endif + public abstract HashAlgorithmType? SslHashAlgorithm { + get; + } + + /// + /// Get the negotiated SSL or TLS hash algorithm strength. + /// + /// + /// Gets the negotiated SSL or TLS hash algorithm strength once an SSL or TLS connection has been made. + /// + /// The negotiated SSL or TLS hash algorithm strength. +#if NET10_0_OR_GREATER + [Obsolete ("Use SslCipherSuite instead.")] +#endif + public abstract int? SslHashStrength { + get; + } + + /// + /// Get the negotiated SSL or TLS key exchange algorithm. + /// + /// + /// Gets the negotiated SSL or TLS key exchange algorithm once an SSL or TLS connection has been made. + /// + /// The negotiated SSL or TLS key exchange algorithm. +#if NET10_0_OR_GREATER + [Obsolete ("Use SslCipherSuite instead.")] +#endif + public abstract ExchangeAlgorithmType? SslKeyExchangeAlgorithm { + get; + } + + /// + /// Get the negotiated SSL or TLS key exchange algorithm strength. + /// + /// + /// Gets the negotiated SSL or TLS key exchange algorithm strength once an SSL or TLS connection has been made. + /// + /// The negotiated SSL or TLS key exchange algorithm strength. +#if NET10_0_OR_GREATER + [Obsolete ("Use SslCipherSuite instead.")] +#endif + public abstract int? SslKeyExchangeStrength { + get; + } + /// /// Get whether or not the client is currently authenticated with the mail server. /// @@ -241,7 +426,7 @@ public abstract bool IsSecure { /// Authenticate methods /// or any of the Async alternatives. /// - /// true if the client is authenticated; otherwise, false. + /// if the client is authenticated; otherwise, . public abstract bool IsAuthenticated { get; } @@ -258,56 +443,75 @@ public abstract int Timeout { get; set; } -#if !NETFX_CORE /// /// The default server certificate validation callback used when connecting via SSL or TLS. /// /// - /// The default server certificate validation callback considers self-signed certificates to be - /// valid so long as the only error in the certificate chain is an untrusted root. - /// It should be noted that self-signed certificates may be an indication of - /// a man-in-the-middle (MITM) attack and so it is recommended that the client implement a custom - /// server certificate validation callback that presents the certificate to the user in some way, - /// allowing the user to confirm or deny its validity. + /// The default server certificate validation callback only succeeds if there are no SSL/TLS certificate validation errors. /// - /// true if the certificate is deemed valid; otherwise, false. + /// if the certificate is deemed valid; otherwise, . /// The object that is connecting via SSL or TLS. /// The server's SSL certificate. /// The server's SSL certificate chain. /// The SSL policy errors. - public static bool DefaultServerCertificateValidationCallback (object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) + protected static bool DefaultServerCertificateValidationCallback (object? sender, X509Certificate? certificate, X509Chain? chain, SslPolicyErrors sslPolicyErrors) { - if (sslPolicyErrors == SslPolicyErrors.None) - return true; - - // if there are errors in the certificate chain, look at each error to determine the cause - if ((sslPolicyErrors & SslPolicyErrors.RemoteCertificateChainErrors) != 0) { - if (chain != null && chain.ChainStatus != null) { - foreach (var status in chain.ChainStatus) { - if ((certificate.Subject == certificate.Issuer) && (status.Status == X509ChainStatusFlags.UntrustedRoot)) { - // treat self-signed certificates with an untrusted root as valid since they are so - // common among mail server installations - continue; - } - - if (status.Status != X509ChainStatusFlags.NoError) { - // if there are any other errors in the certificate chain, the certificate is invalid, - // so return false - return false; - } - } - } - - // Note: If we get this far, then the only errors in the certificate chain are untrusted root errors for - // self-signed certificates. Since self-signed certificates are so common for mail server installations, - // treat the certificate as valid. - return true; - } + return sslPolicyErrors == SslPolicyErrors.None; + } - return false; +#if NET5_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER + /// + /// Gets the SSL/TLS client authentication options for use with .NET5's SslStream.AuthenticateAsClient() API. + /// + /// + /// Gets the SSL/TLS client authentication options for use with .NET5's SslStream.AuthenticateAsClient() API. + /// + /// The target host that the client is connected to. + /// The remote certificate validation callback. + /// The client SSL/TLS authentication options. + protected virtual SslClientAuthenticationOptions GetSslClientAuthenticationOptions (string host, RemoteCertificateValidationCallback remoteCertificateValidationCallback) + { + return new SslClientAuthenticationOptions { + CertificateRevocationCheckMode = CheckCertificateRevocation ? X509RevocationMode.Online : X509RevocationMode.NoCheck, + // Note: Not all servers support Application Protocols, so this will break in some cases. + //ApplicationProtocols = new List { new SslApplicationProtocol (Protocol) }, + RemoteCertificateValidationCallback = remoteCertificateValidationCallback, +#if NET5_0_OR_GREATER + CipherSuitesPolicy = SslCipherSuitesPolicy, +#endif + ClientCertificates = ClientCertificates, + EnabledSslProtocols = SslProtocols, + TargetHost = host + }; } #endif + internal Stream ConnectNetwork (string host, int port, CancellationToken cancellationToken) + { + if (ProxyClient != null) { + ProxyClient.LocalEndPoint = LocalEndPoint; + + return ProxyClient.Connect (host, port, Timeout, cancellationToken); + } + + var socket = SocketUtils.Connect (host, port, LocalEndPoint, Timeout, cancellationToken); + + return new NetworkStream (socket, true); + } + + internal async Task ConnectNetworkAsync (string host, int port, CancellationToken cancellationToken) + { + if (ProxyClient != null) { + ProxyClient.LocalEndPoint = LocalEndPoint; + + return await ProxyClient.ConnectAsync (host, port, Timeout, cancellationToken).ConfigureAwait (false); + } + + var socket = await SocketUtils.ConnectAsync (host, port, LocalEndPoint, Timeout, cancellationToken).ConfigureAwait (false); + + return new NetworkStream (socket, true); + } + /// /// Establish a connection to the specified mail server. /// @@ -322,7 +526,7 @@ public static bool DefaultServerCertificateValidationCallback (object sender, X5 /// The secure socket options to when connecting. /// The cancellation token. /// - /// is null. + /// is . /// /// /// is not between 0 and 65535. @@ -348,7 +552,7 @@ public static bool DefaultServerCertificateValidationCallback (object sender, X5 /// /// A protocol error occurred. /// - public abstract void Connect (string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default (CancellationToken)); + public abstract void Connect (string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default); /// /// Asynchronously establish a connection to the specified mail server. @@ -362,7 +566,7 @@ public static bool DefaultServerCertificateValidationCallback (object sender, X5 /// The secure socket options to when connecting. /// The cancellation token. /// - /// is null. + /// is . /// /// /// is not between 0 and 65535. @@ -388,22 +592,216 @@ public static bool DefaultServerCertificateValidationCallback (object sender, X5 /// /// A protocol error occurred. /// - public virtual Task ConnectAsync (string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default (CancellationToken)) + public abstract Task ConnectAsync (string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default); + + /// + /// Establish a connection to the specified mail server using the provided socket. + /// + /// + /// Establish a connection to the specified mail server using the provided socket. + /// If a successful connection is made, the + /// property will be populated. + /// + /// The socket to use for the connection. + /// The host name to connect to. + /// The port to connect to. If the specified port is 0, then the default port will be used. + /// The secure socket options to when connecting. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// is not between 0 and 65535. + /// + /// + /// is not connected. + /// -or- + /// The is a zero-length string. + /// + /// + /// The is already connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command was rejected by the mail server. + /// + /// + /// The server responded with an unexpected token. + /// + public abstract void Connect (Socket socket, string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default); + + /// + /// Asynchronously establish a connection to the specified mail server using the provided socket. + /// + /// + /// Asynchronously establishes a connection to the specified mail server using the provided socket. + /// If a successful connection is made, the + /// property will be populated. + /// + /// An asynchronous task context. + /// The socket to use for the connection. + /// The host name to connect to. + /// The port to connect to. If the specified port is 0, then the default port will be used. + /// The secure socket options to when connecting. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// is not between 0 and 65535. + /// + /// + /// is not connected. + /// -or- + /// The is a zero-length string. + /// + /// + /// The is already connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command was rejected by the mail server. + /// + /// + /// The server responded with an unexpected token. + /// + public abstract Task ConnectAsync (Socket socket, string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default); + + /// + /// Establish a connection to the specified mail server using the provided stream. + /// + /// + /// Establish a connection to the specified mail server using the provided stream. + /// If a successful connection is made, the + /// property will be populated. + /// + /// The stream to use for the connection. + /// The host name to connect to. + /// The port to connect to. If the specified port is 0, then the default port will be used. + /// The secure socket options to when connecting. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The is already connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command was rejected by the mail server. + /// + /// + /// The server responded with an unexpected token. + /// + public abstract void Connect (Stream stream, string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default); + + /// + /// Asynchronously establish a connection to the specified mail server using the provided stream. + /// + /// + /// Asynchronously establishes a connection to the specified mail server using the provided stream. + /// If a successful connection is made, the + /// property will be populated. + /// + /// An asynchronous task context. + /// The stream to use for the connection. + /// The host name to connect to. + /// The port to connect to. If the specified port is 0, then the default port will be used. + /// The secure socket options to when connecting. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The is already connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command was rejected by the mail server. + /// + /// + /// The server responded with an unexpected token. + /// + public abstract Task ConnectAsync (Stream stream, string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default); + + static bool IsAny (string value, params string[] anyOf) { - if (host == null) - throw new ArgumentNullException (nameof (host)); + foreach (var item in anyOf) { + if (value.Equals (item, StringComparison.OrdinalIgnoreCase)) + return true; + } - if (host.Length == 0) - throw new ArgumentException ("The host name cannot be empty.", nameof (host)); + return false; + } - if (port < 0 || port > 65535) - throw new ArgumentOutOfRangeException (nameof (port)); + internal SecureSocketOptions GetSecureSocketOptions (Uri uri) + { + var query = uri.ParsedQuery (); + var protocol = uri.Scheme; + + // Note: early versions of MailKit used "pop3" and "pop3s" + if (protocol.Equals ("pop3s", StringComparison.OrdinalIgnoreCase)) + protocol = "pops"; + else if (protocol.Equals ("pop3", StringComparison.OrdinalIgnoreCase)) + protocol = "pop"; + + if (protocol.Equals (Protocol + "s", StringComparison.OrdinalIgnoreCase)) + return SecureSocketOptions.SslOnConnect; + + if (!protocol.Equals (Protocol, StringComparison.OrdinalIgnoreCase)) + throw new ArgumentException ("Unknown URI scheme.", nameof (uri)); - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - Connect (host, port, options, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); + if (query.TryGetValue ("starttls", out string? value)) { + if (IsAny (value, "always", "true", "yes")) + return SecureSocketOptions.StartTls; + + if (IsAny (value, "never", "false", "no")) + return SecureSocketOptions.None; + + return SecureSocketOptions.StartTlsWhenAvailable; + } + + return SecureSocketOptions.StartTlsWhenAvailable; } /// @@ -418,7 +816,7 @@ public static bool DefaultServerCertificateValidationCallback (object sender, X5 /// The server URI. /// The cancellation token. /// - /// The is null. + /// The is . /// /// /// The is not an absolute URI. @@ -441,7 +839,7 @@ public static bool DefaultServerCertificateValidationCallback (object sender, X5 /// /// A protocol error occurred. /// - public void Connect (Uri uri, CancellationToken cancellationToken = default (CancellationToken)) + public void Connect (Uri uri, CancellationToken cancellationToken = default) { if (uri == null) throw new ArgumentNullException (nameof (uri)); @@ -449,36 +847,7 @@ public static bool DefaultServerCertificateValidationCallback (object sender, X5 if (!uri.IsAbsoluteUri) throw new ArgumentException ("The uri must be absolute.", nameof (uri)); - var protocol = uri.Scheme.ToLowerInvariant (); - var query = uri.ParsedQuery (); - SecureSocketOptions options; - string value; - - // Note: early versions of MailKit used "pop3" and "pop3s" - if (protocol == "pop3s") - protocol = "pops"; - else if (protocol == "pop3") - protocol = "pop"; - - if (protocol == Protocol + "s") { - options = SecureSocketOptions.SslOnConnect; - } else if (protocol != Protocol) { - throw new ArgumentException ("Unknown URI scheme.", nameof (uri)); - } else if (query.TryGetValue ("starttls", out value)) { - switch (value.ToLowerInvariant ()) { - default: - options = SecureSocketOptions.StartTlsWhenAvailable; - break; - case "always": case "true": case "yes": - options = SecureSocketOptions.StartTls; - break; - case "never": case "false": case "no": - options = SecureSocketOptions.None; - break; - } - } else { - options = SecureSocketOptions.StartTlsWhenAvailable; - } + var options = GetSecureSocketOptions (uri); Connect (uri.Host, uri.Port < 0 ? 0 : uri.Port, options, cancellationToken); } @@ -493,7 +862,7 @@ public static bool DefaultServerCertificateValidationCallback (object sender, X5 /// The server URI. /// The cancellation token. /// - /// The is null. + /// The is . /// /// /// The is not an absolute URI. @@ -516,7 +885,7 @@ public static bool DefaultServerCertificateValidationCallback (object sender, X5 /// /// A protocol error occurred. /// - public Task ConnectAsync (Uri uri, CancellationToken cancellationToken = default (CancellationToken)) + public Task ConnectAsync (Uri uri, CancellationToken cancellationToken = default) { if (uri == null) throw new ArgumentNullException (nameof (uri)); @@ -524,11 +893,9 @@ public static bool DefaultServerCertificateValidationCallback (object sender, X5 if (!uri.IsAbsoluteUri) throw new ArgumentException ("The uri must be absolute.", nameof (uri)); - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - Connect (uri, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); + var options = GetSecureSocketOptions (uri); + + return ConnectAsync (uri.Host, uri.Port < 0 ? 0 : uri.Port, options, cancellationToken); } /// @@ -539,7 +906,7 @@ public static bool DefaultServerCertificateValidationCallback (object sender, X5 /// /// The argument only controls whether or /// not the client makes an SSL-wrapped connection. In other words, even if the - /// parameter is false, SSL/TLS may still be used if + /// parameter is , SSL/TLS may still be used if /// the mail server supports the STARTTLS extension. /// To disable all use of SSL/TLS, use the /// @@ -550,10 +917,10 @@ public static bool DefaultServerCertificateValidationCallback (object sender, X5 /// /// The host to connect to. /// The port to connect to. If the specified port is 0, then the default port will be used. - /// true if the client should make an SSL-wrapped connection to the server; otherwise, false. + /// if the client should make an SSL-wrapped connection to the server; otherwise, . /// The cancellation token. /// - /// The is null. + /// The is . /// /// /// is out of range (0 to 65535, inclusive). @@ -579,7 +946,7 @@ public static bool DefaultServerCertificateValidationCallback (object sender, X5 /// /// A protocol error occurred. /// - public void Connect (string host, int port, bool useSsl, CancellationToken cancellationToken = default (CancellationToken)) + public void Connect (string host, int port, bool useSsl, CancellationToken cancellationToken = default) { if (host == null) throw new ArgumentNullException (nameof (host)); @@ -601,7 +968,7 @@ public static bool DefaultServerCertificateValidationCallback (object sender, X5 /// /// The argument only controls whether or /// not the client makes an SSL-wrapped connection. In other words, even if the - /// parameter is false, SSL/TLS may still be used if + /// parameter is , SSL/TLS may still be used if /// the mail server supports the STARTTLS extension. /// To disable all use of SSL/TLS, use the /// @@ -613,10 +980,10 @@ public static bool DefaultServerCertificateValidationCallback (object sender, X5 /// An asynchronous task context. /// The host to connect to. /// The port to connect to. If the specified port is 0, then the default port will be used. - /// true if the client should make an SSL-wrapped connection to the server; otherwise, false. + /// if the client should make an SSL-wrapped connection to the server; otherwise, . /// The cancellation token. /// - /// The is null. + /// The is . /// /// /// is out of range (0 to 65535, inclusive). @@ -642,7 +1009,7 @@ public static bool DefaultServerCertificateValidationCallback (object sender, X5 /// /// A protocol error occurred. /// - public Task ConnectAsync (string host, int port, bool useSsl, CancellationToken cancellationToken = default (CancellationToken)) + public Task ConnectAsync (string host, int port, bool useSsl, CancellationToken cancellationToken = default) { if (host == null) throw new ArgumentNullException (nameof (host)); @@ -657,13 +1024,14 @@ public static bool DefaultServerCertificateValidationCallback (object sender, X5 } /// - /// Authenticates using the supplied credentials. + /// Authenticate using the supplied credentials. /// /// - /// If the server supports one or more SASL authentication mechanisms, - /// then the SASL mechanisms that both the client and server support are tried - /// in order of greatest security to weakest security. Once a SASL - /// authentication mechanism is found that both client and server support, + /// Authenticates using the supplied credentials. + /// If the server supports one or more SASL authentication mechanisms, then + /// the SASL mechanisms that both the client and server support (not including any + /// OAUTH mechanisms) are tried in order of greatest security to weakest security. + /// Once a SASL authentication mechanism is found that both client and server support, /// the credentials are used to authenticate. /// If the server does not support SASL or if no common SASL mechanisms /// can be found, then the default login command is used as a fallback. @@ -675,9 +1043,9 @@ public static bool DefaultServerCertificateValidationCallback (object sender, X5 /// The user's credentials. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// The has been disposed. @@ -700,16 +1068,17 @@ public static bool DefaultServerCertificateValidationCallback (object sender, X5 /// /// A protocol error occurred. /// - public abstract void Authenticate (Encoding encoding, ICredentials credentials, CancellationToken cancellationToken = default (CancellationToken)); + public abstract void Authenticate (Encoding encoding, ICredentials credentials, CancellationToken cancellationToken = default); /// - /// Asynchronously authenticates using the supplied credentials. + /// Asynchronously authenticate using the supplied credentials. /// /// - /// If the server supports one or more SASL authentication mechanisms, - /// then the SASL mechanisms that both the client and server support are tried - /// in order of greatest security to weakest security. Once a SASL - /// authentication mechanism is found that both client and server support, + /// Asynchronously authenticates using the supplied credentials. + /// If the server supports one or more SASL authentication mechanisms, then + /// the SASL mechanisms that both the client and server support (not including any + /// OAUTH mechanisms) are tried in order of greatest security to weakest security. + /// Once a SASL authentication mechanism is found that both client and server support, /// the credentials are used to authenticate. /// If the server does not support SASL or if no common SASL mechanisms /// can be found, then the default login command is used as a fallback. @@ -722,9 +1091,9 @@ public static bool DefaultServerCertificateValidationCallback (object sender, X5 /// The user's credentials. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// The has been disposed. @@ -747,29 +1116,17 @@ public static bool DefaultServerCertificateValidationCallback (object sender, X5 /// /// A protocol error occurred. /// - public virtual Task AuthenticateAsync (Encoding encoding, ICredentials credentials, CancellationToken cancellationToken = default (CancellationToken)) - { - if (encoding == null) - throw new ArgumentNullException (nameof (encoding)); - - if (credentials == null) - throw new ArgumentNullException (nameof (credentials)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - Authenticate (encoding, credentials, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task AuthenticateAsync (Encoding encoding, ICredentials credentials, CancellationToken cancellationToken = default); /// - /// Authenticates using the supplied credentials. + /// Authenticate using the supplied credentials. /// /// - /// If the server supports one or more SASL authentication mechanisms, - /// then the SASL mechanisms that both the client and server support are tried - /// in order of greatest security to weakest security. Once a SASL - /// authentication mechanism is found that both client and server support, + /// Authenticates using the supplied credentials. + /// If the server supports one or more SASL authentication mechanisms, then + /// the SASL mechanisms that both the client and server support (not including any + /// OAUTH mechanisms) are tried in order of greatest security to weakest security. + /// Once a SASL authentication mechanism is found that both client and server support, /// the credentials are used to authenticate. /// If the server does not support SASL or if no common SASL mechanisms /// can be found, then the default login command is used as a fallback. @@ -780,7 +1137,7 @@ public static bool DefaultServerCertificateValidationCallback (object sender, X5 /// The user's credentials. /// The cancellation token. /// - /// is null. + /// is . /// /// /// The has been disposed. @@ -803,19 +1160,20 @@ public static bool DefaultServerCertificateValidationCallback (object sender, X5 /// /// A protocol error occurred. /// - public void Authenticate (ICredentials credentials, CancellationToken cancellationToken = default (CancellationToken)) + public void Authenticate (ICredentials credentials, CancellationToken cancellationToken = default) { Authenticate (Encoding.UTF8, credentials, cancellationToken); } /// - /// Asynchronously authenticates using the supplied credentials. + /// Asynchronously authenticate using the supplied credentials. /// /// - /// If the server supports one or more SASL authentication mechanisms, - /// then the SASL mechanisms that both the client and server support are tried - /// in order of greatest security to weakest security. Once a SASL - /// authentication mechanism is found that both client and server support, + /// Asynchronously authenticates using the supplied credentials. + /// If the server supports one or more SASL authentication mechanisms, then + /// the SASL mechanisms that both the client and server support (not including any + /// OAUTH mechanisms) are tried in order of greatest security to weakest security. + /// Once a SASL authentication mechanism is found that both client and server support, /// the credentials are used to authenticate. /// If the server does not support SASL or if no common SASL mechanisms /// can be found, then the default login command is used as a fallback. @@ -827,7 +1185,7 @@ public static bool DefaultServerCertificateValidationCallback (object sender, X5 /// The user's credentials. /// The cancellation token. /// - /// is null. + /// is . /// /// /// The has been disposed. @@ -850,22 +1208,20 @@ public static bool DefaultServerCertificateValidationCallback (object sender, X5 /// /// A protocol error occurred. /// - public Task AuthenticateAsync (ICredentials credentials, CancellationToken cancellationToken = default (CancellationToken)) + public Task AuthenticateAsync (ICredentials credentials, CancellationToken cancellationToken = default) { - if (credentials == null) - throw new ArgumentNullException (nameof (credentials)); - return AuthenticateAsync (Encoding.UTF8, credentials, cancellationToken); } /// - /// Authenticates using the specified user name and password. + /// Authenticate using the specified user name and password. /// /// - /// If the server supports one or more SASL authentication mechanisms, - /// then the SASL mechanisms that both the client and server support are tried - /// in order of greatest security to weakest security. Once a SASL - /// authentication mechanism is found that both client and server support, + /// Authenticates using the supplied credentials. + /// If the server supports one or more SASL authentication mechanisms, then + /// the SASL mechanisms that both the client and server support (not including any + /// OAUTH mechanisms) are tried in order of greatest security to weakest security. + /// Once a SASL authentication mechanism is found that both client and server support, /// the credentials are used to authenticate. /// If the server does not support SASL or if no common SASL mechanisms /// can be found, then the default login command is used as a fallback. @@ -878,11 +1234,11 @@ public static bool DefaultServerCertificateValidationCallback (object sender, X5 /// The password. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// The has been disposed. @@ -905,7 +1261,7 @@ public static bool DefaultServerCertificateValidationCallback (object sender, X5 /// /// A protocol error occurred. /// - public void Authenticate (Encoding encoding, string userName, string password, CancellationToken cancellationToken = default (CancellationToken)) + public void Authenticate (Encoding encoding, string userName, string password, CancellationToken cancellationToken = default) { if (encoding == null) throw new ArgumentNullException (nameof (encoding)); @@ -922,13 +1278,14 @@ public static bool DefaultServerCertificateValidationCallback (object sender, X5 } /// - /// Asynchronously authenticates using the specified user name and password. + /// Asynchronously authenticate using the specified user name and password. /// /// - /// If the server supports one or more SASL authentication mechanisms, - /// then the SASL mechanisms that both the client and server support are tried - /// in order of greatest security to weakest security. Once a SASL - /// authentication mechanism is found that both client and server support, + /// Asynchronously authenticates using the supplied credentials. + /// If the server supports one or more SASL authentication mechanisms, then + /// the SASL mechanisms that both the client and server support (not including any + /// OAUTH mechanisms) are tried in order of greatest security to weakest security. + /// Once a SASL authentication mechanism is found that both client and server support, /// the credentials are used to authenticate. /// If the server does not support SASL or if no common SASL mechanisms /// can be found, then the default login command is used as a fallback. @@ -942,11 +1299,11 @@ public static bool DefaultServerCertificateValidationCallback (object sender, X5 /// The password. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// The has been disposed. @@ -969,7 +1326,7 @@ public static bool DefaultServerCertificateValidationCallback (object sender, X5 /// /// A protocol error occurred. /// - public Task AuthenticateAsync (Encoding encoding, string userName, string password, CancellationToken cancellationToken = default (CancellationToken)) + public Task AuthenticateAsync (Encoding encoding, string userName, string password, CancellationToken cancellationToken = default) { if (encoding == null) throw new ArgumentNullException (nameof (encoding)); @@ -986,13 +1343,14 @@ public static bool DefaultServerCertificateValidationCallback (object sender, X5 } /// - /// Authenticates using the specified user name and password. + /// Authenticate using the specified user name and password. /// /// - /// If the server supports one or more SASL authentication mechanisms, - /// then the SASL mechanisms that both the client and server support are tried - /// in order of greatest security to weakest security. Once a SASL - /// authentication mechanism is found that both client and server support, + /// Authenticates using the supplied credentials. + ///If the server supports one or more SASL authentication mechanisms, then + /// the SASL mechanisms that both the client and server support (not including any + /// OAUTH mechanisms) are tried in order of greatest security to weakest security. + /// Once a SASL authentication mechanism is found that both client and server support, /// the credentials are used to authenticate. /// If the server does not support SASL or if no common SASL mechanisms /// can be found, then the default login command is used as a fallback. @@ -1007,9 +1365,9 @@ public static bool DefaultServerCertificateValidationCallback (object sender, X5 /// The password. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// The has been disposed. @@ -1032,27 +1390,20 @@ public static bool DefaultServerCertificateValidationCallback (object sender, X5 /// /// A protocol error occurred. /// - public void Authenticate (string userName, string password, CancellationToken cancellationToken = default (CancellationToken)) + public void Authenticate (string userName, string password, CancellationToken cancellationToken = default) { - if (userName == null) - throw new ArgumentNullException (nameof (userName)); - - if (password == null) - throw new ArgumentNullException (nameof (password)); - - var credentials = new NetworkCredential (userName, password); - - Authenticate (Encoding.UTF8, credentials, cancellationToken); + Authenticate (Encoding.UTF8, userName, password, cancellationToken); } /// - /// Asynchronously authenticates using the specified user name and password. + /// Asynchronously authenticate using the specified user name and password. /// /// - /// If the server supports one or more SASL authentication mechanisms, - /// then the SASL mechanisms that both the client and server support are tried - /// in order of greatest security to weakest security. Once a SASL - /// authentication mechanism is found that both client and server support, + /// Asynchronously authenticates using the supplied credentials. + /// If the server supports one or more SASL authentication mechanisms, then + /// the SASL mechanisms that both the client and server support (not including any + /// OAUTH mechanisms) are tried in order of greatest security to weakest security. + /// Once a SASL authentication mechanism is found that both client and server support, /// the credentials are used to authenticate. /// If the server does not support SASL or if no common SASL mechanisms /// can be found, then the default login command is used as a fallback. @@ -1065,9 +1416,9 @@ public static bool DefaultServerCertificateValidationCallback (object sender, X5 /// The password. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// The has been disposed. @@ -1090,58 +1441,118 @@ public static bool DefaultServerCertificateValidationCallback (object sender, X5 /// /// A protocol error occurred. /// - public Task AuthenticateAsync (string userName, string password, CancellationToken cancellationToken = default (CancellationToken)) + public Task AuthenticateAsync (string userName, string password, CancellationToken cancellationToken = default) { - if (userName == null) - throw new ArgumentNullException (nameof (userName)); - - if (password == null) - throw new ArgumentNullException (nameof (password)); + return AuthenticateAsync (Encoding.UTF8, userName, password, cancellationToken); + } - var credentials = new NetworkCredential (userName, password); + /// + /// Authenticate using the specified SASL mechanism. + /// + /// + /// Authenticates using the specified SASL mechanism. + /// For a list of available SASL authentication mechanisms supported by the server, + /// check the property after the service has been + /// connected. + /// + /// The SASL mechanism. + /// The cancellation token. + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected or is already authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Authentication using the supplied credentials has failed. + /// + /// + /// A SASL authentication error occurred. + /// + /// + /// An I/O error occurred. + /// + /// + /// A protocol error occurred. + /// + public abstract void Authenticate (SaslMechanism mechanism, CancellationToken cancellationToken = default); - return AuthenticateAsync (Encoding.UTF8, credentials, cancellationToken); - } + /// + /// Asynchronously authenticate using the specified SASL mechanism. + /// + /// + /// Authenticates using the specified SASL mechanism. + /// For a list of available SASL authentication mechanisms supported by the server, + /// check the property after the service has been + /// connected. + /// + /// An asynchronous task context. + /// The SASL mechanism. + /// The cancellation token. + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected or is already authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Authentication using the supplied credentials has failed. + /// + /// + /// A SASL authentication error occurred. + /// + /// + /// An I/O error occurred. + /// + /// + /// A protocol error occurred. + /// + public abstract Task AuthenticateAsync (SaslMechanism mechanism, CancellationToken cancellationToken = default); /// - /// Disconnects the service. + /// Disconnect the service. /// /// - /// If is true, a logout/quit command will be issued in order to disconnect cleanly. + /// If is , a logout/quit command will be issued in order to disconnect cleanly. /// /// /// /// - /// If set to true, a logout/quit command will be issued in order to disconnect cleanly. + /// If set to , a logout/quit command will be issued in order to disconnect cleanly. /// The cancellation token. /// /// The has been disposed. /// - public abstract void Disconnect (bool quit, CancellationToken cancellationToken = default (CancellationToken)); + public abstract void Disconnect (bool quit, CancellationToken cancellationToken = default); /// - /// Asynchronously disconnects the service. + /// Asynchronously disconnect the service. /// /// - /// If is true, a logout/quit command will be issued in order to disconnect cleanly. + /// If is , a logout/quit command will be issued in order to disconnect cleanly. /// /// An asynchronous task context. - /// If set to true, a logout/quit command will be issued in order to disconnect cleanly. + /// If set to , a logout/quit command will be issued in order to disconnect cleanly. /// The cancellation token. /// /// The has been disposed. /// - public virtual Task DisconnectAsync (bool quit, CancellationToken cancellationToken = default (CancellationToken)) - { - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - Disconnect (quit, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task DisconnectAsync (bool quit, CancellationToken cancellationToken = default); /// - /// Pings the mail server to keep the connection alive. + /// Ping the mail server to keep the connection alive. /// /// /// Mail servers, if left idle for too long, will automatically drop the connection. @@ -1167,10 +1578,10 @@ public static bool DefaultServerCertificateValidationCallback (object sender, X5 /// /// The server responded with an unexpected token. /// - public abstract void NoOp (CancellationToken cancellationToken = default (CancellationToken)); + public abstract void NoOp (CancellationToken cancellationToken = default); /// - /// Asynchronously pings the mail server to keep the connection alive. + /// Asynchronously ping the mail server to keep the connection alive. /// /// /// Mail servers, if left idle for too long, will automatically drop the connection. @@ -1197,14 +1608,7 @@ public static bool DefaultServerCertificateValidationCallback (object sender, X5 /// /// The server responded with an unexpected token. /// - public virtual Task NoOpAsync (CancellationToken cancellationToken = default (CancellationToken)) - { - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - NoOp (cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task NoOpAsync (CancellationToken cancellationToken = default); /// /// Occurs when the client has been successfully connected. @@ -1213,7 +1617,7 @@ public static bool DefaultServerCertificateValidationCallback (object sender, X5 /// The event is raised when the client /// successfully connects to the mail server. /// - public event EventHandler Connected; + public event EventHandler? Connected; /// /// Raise the connected event. @@ -1221,12 +1625,12 @@ public static bool DefaultServerCertificateValidationCallback (object sender, X5 /// /// Raises the connected event. /// - protected virtual void OnConnected () + /// The name of the host that the client connected to. + /// The port that the client connected to on the remote host. + /// The SSL/TLS options that were used when connecting. + protected virtual void OnConnected (string host, int port, SecureSocketOptions options) { - var handler = Connected; - - if (handler != null) - handler (this, EventArgs.Empty); + Connected?.Invoke (this, new ConnectedEventArgs (host, port, options)); } /// @@ -1236,7 +1640,7 @@ protected virtual void OnConnected () /// The event is raised whenever the client /// gets disconnected. /// - public event EventHandler Disconnected; + public event EventHandler? Disconnected; /// /// Raise the disconnected event. @@ -1244,12 +1648,13 @@ protected virtual void OnConnected () /// /// Raises the disconnected event. /// - protected virtual void OnDisconnected () + /// The name of the host that the client was connected to. + /// The port that the client was connected to on the remote host. + /// The SSL/TLS options that were used by the client. + /// if the disconnect was explicitly requested; otherwise, . + protected virtual void OnDisconnected (string host, int port, SecureSocketOptions options, bool requested) { - var handler = Disconnected; - - if (handler != null) - handler (this, EventArgs.Empty); + Disconnected?.Invoke (this, new DisconnectedEventArgs (host, port, options, requested)); } /// @@ -1259,7 +1664,7 @@ protected virtual void OnDisconnected () /// The event is raised whenever the client /// has been authenticated. /// - public event EventHandler Authenticated; + public event EventHandler? Authenticated; /// /// Raise the authenticated event. @@ -1270,10 +1675,7 @@ protected virtual void OnDisconnected () /// The notification sent by the server when the client successfully authenticates. protected virtual void OnAuthenticated (string message) { - var handler = Authenticated; - - if (handler != null) - handler (this, new AuthenticatedEventArgs (message)); + Authenticated?.Invoke (this, new AuthenticatedEventArgs (message)); } /// @@ -1284,8 +1686,8 @@ protected virtual void OnAuthenticated (string message) /// Releases the unmanaged resources used by the and /// optionally releases the managed resources. /// - /// true to release both managed and unmanaged resources; - /// false to release only the unmanaged resources. + /// to release both managed and unmanaged resources; + /// to release only the unmanaged resources. protected virtual void Dispose (bool disposing) { if (disposing) diff --git a/MailKit/MailSpool.cs b/MailKit/MailSpool.cs index a9100daeed..ae58ae5598 100644 --- a/MailKit/MailSpool.cs +++ b/MailKit/MailSpool.cs @@ -1,9 +1,9 @@ -// +// // MailSpool.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -50,7 +50,7 @@ public abstract class MailSpool : MailService, IMailSpool /// /// The protocol logger. /// - /// is null. + /// is . /// protected MailSpool (IProtocolLogger protocolLogger) : base (protocolLogger) { @@ -92,7 +92,7 @@ public abstract int Count { /// along with and /// will fail. /// - /// true if supports uids; otherwise, false. + /// if supports uids; otherwise, . /// /// The has been disposed. /// @@ -107,12 +107,12 @@ public abstract bool SupportsUids { } /// - /// Get the number of messages available in the message spool. + /// Get the message count. /// /// - /// Gets the number of messages available in the message spool. + /// Gets the message count. /// - /// The number of available messages. + /// The message count. /// The cancellation token. /// /// The has been disposed. @@ -135,16 +135,15 @@ public abstract bool SupportsUids { /// /// A protocol error occurred. /// - [Obsolete ("Use the Count property instead.")] - public abstract int GetMessageCount (CancellationToken cancellationToken = default (CancellationToken)); + public abstract int GetMessageCount (CancellationToken cancellationToken = default); /// - /// Asynchronously get the number of messages available in the message spool. + /// Asynchronously get the message count. /// /// - /// Asynchronously gets the number of messages available in the message spool. + /// Asynchronously gets the message count. /// - /// The number of available messages. + /// The message count. /// The cancellation token. /// /// The has been disposed. @@ -167,15 +166,7 @@ public abstract bool SupportsUids { /// /// A protocol error occurred. /// - [Obsolete ("Use the Count property instead.")] - public virtual Task GetMessageCountAsync (CancellationToken cancellationToken = default (CancellationToken)) - { - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return GetMessageCount (cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task GetMessageCountAsync (CancellationToken cancellationToken = default); /// /// Get the UID of the message at the specified index. @@ -214,7 +205,7 @@ public abstract bool SupportsUids { /// /// A protocol error occurred. /// - public abstract string GetMessageUid (int index, CancellationToken cancellationToken = default (CancellationToken)); + public abstract string GetMessageUid (int index, CancellationToken cancellationToken = default); /// /// Asynchronously get the UID of the message at the specified index. @@ -253,14 +244,7 @@ public abstract bool SupportsUids { /// /// A protocol error occurred. /// - public virtual Task GetMessageUidAsync (int index, CancellationToken cancellationToken = default (CancellationToken)) - { - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return GetMessageUid (index, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task GetMessageUidAsync (int index, CancellationToken cancellationToken = default); /// /// Get the full list of available message UIDs. @@ -298,7 +282,7 @@ public abstract bool SupportsUids { /// /// A protocol error occurred. /// - public abstract IList GetMessageUids (CancellationToken cancellationToken = default (CancellationToken)); + public abstract IList GetMessageUids (CancellationToken cancellationToken = default); /// /// Get the full list of available message UIDs. @@ -333,102 +317,7 @@ public abstract bool SupportsUids { /// /// A protocol error occurred. /// - public virtual Task> GetMessageUidsAsync (CancellationToken cancellationToken = default (CancellationToken)) - { - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return GetMessageUids (cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Get the size of the specified message, in bytes. - /// - /// - /// Gets the size of the specified message, in bytes. - /// - /// The message size, in bytes. - /// The UID of the message. - /// The cancellation token. - /// - /// is null. - /// - /// - /// is not a valid message UID. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The command failed. - /// - /// - /// A protocol error occurred. - /// - [Obsolete ("Use GetMessageSize (int index, CancellationToken cancellationToken = default (CancellationToken)) instead.")] - public abstract int GetMessageSize (string uid, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously get the size of the specified message, in bytes. - /// - /// - /// Asynchronously gets the size of the specified message, in bytes. - /// - /// The message size, in bytes. - /// The UID of the message. - /// The cancellation token. - /// - /// is null. - /// - /// - /// is not a valid message UID. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The command failed. - /// - /// - /// A protocol error occurred. - /// - [Obsolete ("Use GetMessageSizeAsync (int index, CancellationToken cancellationToken = default (CancellationToken)) instead.")] - public virtual Task GetMessageSizeAsync (string uid, CancellationToken cancellationToken = default (CancellationToken)) - { - if (uid == null) - throw new ArgumentNullException (nameof (uid)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return GetMessageSize (uid, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task> GetMessageUidsAsync (CancellationToken cancellationToken = default); /// /// Get the size of the specified message, in bytes. @@ -463,7 +352,7 @@ public abstract bool SupportsUids { /// /// A protocol error occurred. /// - public abstract int GetMessageSize (int index, CancellationToken cancellationToken = default (CancellationToken)); + public abstract int GetMessageSize (int index, CancellationToken cancellationToken = default); /// /// Asynchronously get the size of the specified message, in bytes. @@ -498,14 +387,7 @@ public abstract bool SupportsUids { /// /// A protocol error occurred. /// - public virtual Task GetMessageSizeAsync (int index, CancellationToken cancellationToken = default (CancellationToken)) - { - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return GetMessageSize (index, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task GetMessageSizeAsync (int index, CancellationToken cancellationToken = default); /// /// Get the sizes for all available messages, in bytes. @@ -536,7 +418,7 @@ public abstract bool SupportsUids { /// /// A protocol error occurred. /// - public abstract IList GetMessageSizes (CancellationToken cancellationToken = default (CancellationToken)); + public abstract IList GetMessageSizes (CancellationToken cancellationToken = default); /// /// Asynchronously get the sizes for all available messages, in bytes. @@ -567,108 +449,7 @@ public abstract bool SupportsUids { /// /// A protocol error occurred. /// - public virtual Task> GetMessageSizesAsync (CancellationToken cancellationToken = default (CancellationToken)) - { - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return GetMessageSizes (cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Get the headers for the specified message. - /// - /// - /// Gets the headers for the specified message. - /// - /// The message headers. - /// The UID of the message. - /// The cancellation token. - /// - /// is null. - /// - /// - /// is not a valid message UID. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The mail spool does not support UIDs. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The command failed. - /// - /// - /// A protocol error occurred. - /// - [Obsolete ("Use GetMessageHeaders (int index, CancellationToken cancellationToken = default (CancellationToken)) instead.")] - public abstract HeaderList GetMessageHeaders (string uid, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously get the headers for the specified message. - /// - /// - /// Asynchronously gets the headers for the specified message. - /// - /// The message headers. - /// The UID of the message. - /// The cancellation token. - /// - /// is null. - /// - /// - /// is not a valid message UID. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The mail spool does not support UIDs. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The command failed. - /// - /// - /// A protocol error occurred. - /// - [Obsolete ("Use GetMessageHeadersAsync (int index, CancellationToken cancellationToken = default (CancellationToken)) instead.")] - public virtual Task GetMessageHeadersAsync (string uid, CancellationToken cancellationToken = default (CancellationToken)) - { - if (uid == null) - throw new ArgumentNullException (nameof (uid)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return GetMessageHeaders (uid, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task> GetMessageSizesAsync (CancellationToken cancellationToken = default); /// /// Get the headers for the specified message. @@ -703,7 +484,7 @@ public abstract bool SupportsUids { /// /// A protocol error occurred. /// - public abstract HeaderList GetMessageHeaders (int index, CancellationToken cancellationToken = default (CancellationToken)); + public abstract HeaderList GetMessageHeaders (int index, CancellationToken cancellationToken = default); /// /// Asynchronously get the headers for the specified message. @@ -738,115 +519,7 @@ public abstract bool SupportsUids { /// /// A protocol error occurred. /// - public virtual Task GetMessageHeadersAsync (int index, CancellationToken cancellationToken = default (CancellationToken)) - { - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return GetMessageHeaders (index, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Get the headers for the specified messages. - /// - /// - /// Gets the headers for the specified messages. - /// - /// The headers for the specified messages. - /// The UIDs of the messages. - /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the are invalid. - /// -or- - /// No uids were specified. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The mail spool does not support UIDs. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The command failed. - /// - /// - /// A protocol error occurred. - /// - [Obsolete ("Use GetMessageHeaders (IList indexes, CancellationToken cancellationToken = default (CancellationToken)) instead.")] - public abstract IList GetMessageHeaders (IList uids, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously get the headers for the specified messages. - /// - /// - /// Asynchronously gets the headers for the specified messages. - /// - /// The headers for the specified messages. - /// The UIDs of the message. - /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the are invalid. - /// -or- - /// No uids were specified. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The mail spool does not support UIDs. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The command failed. - /// - /// - /// A protocol error occurred. - /// - [Obsolete ("Use GetMessageHeadersAsync (IList indexes, CancellationToken cancellationToken = default (CancellationToken)) instead.")] - public virtual Task> GetMessageHeadersAsync (IList uids, CancellationToken cancellationToken = default (CancellationToken)) - { - if (uids == null) - throw new ArgumentNullException (nameof (uids)); - - if (uids.Count == 0) - throw new ArgumentException ("No uids specified.", nameof (uids)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return GetMessageHeaders (uids, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task GetMessageHeadersAsync (int index, CancellationToken cancellationToken = default); /// /// Get the headers for the specified messages. @@ -858,7 +531,7 @@ public abstract bool SupportsUids { /// The indexes of the messages. /// The cancellation token. /// - /// is null. + /// is . /// /// /// One or more of the are invalid. @@ -886,7 +559,7 @@ public abstract bool SupportsUids { /// /// A protocol error occurred. /// - public abstract IList GetMessageHeaders (IList indexes, CancellationToken cancellationToken = default (CancellationToken)); + public abstract IList GetMessageHeaders (IList indexes, CancellationToken cancellationToken = default); /// /// Asynchronously get the headers for the specified messages. @@ -898,7 +571,7 @@ public abstract bool SupportsUids { /// The indexes of the messages. /// The cancellation token. /// - /// is null. + /// is . /// /// /// One or more of the are invalid. @@ -926,211 +599,21 @@ public abstract bool SupportsUids { /// /// A protocol error occurred. /// - public virtual Task> GetMessageHeadersAsync (IList indexes, CancellationToken cancellationToken = default (CancellationToken)) - { - if (indexes == null) - throw new ArgumentNullException (nameof (indexes)); - - if (indexes.Count == 0) - throw new ArgumentException ("No indexes specified.", nameof (indexes)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return GetMessageHeaders (indexes, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task> GetMessageHeadersAsync (IList indexes, CancellationToken cancellationToken = default); /// /// Get the headers of the messages within the specified range. /// /// - /// Gets the headers of the messages within the specified range. - /// - /// The headers of the messages within the specified range. - /// The index of the first message to get. - /// The number of messages to get. - /// The cancellation token. - /// - /// and do not specify - /// a valid range of messages. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The command failed. - /// - /// - /// A protocol error occurred. - /// - public abstract IList GetMessageHeaders (int startIndex, int count, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Get the headers of the messages within the specified range. - /// - /// - /// Gets the headers of the messages within the specified range. - /// - /// The headers of the messages within the specified range. - /// The index of the first message to get. - /// The number of messages to get. - /// The cancellation token. - /// - /// and do not specify - /// a valid range of messages. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The command failed. - /// - /// - /// A protocol error occurred. - /// - public virtual Task> GetMessageHeadersAsync (int startIndex, int count, CancellationToken cancellationToken = default (CancellationToken)) - { - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return GetMessageHeaders (startIndex, count, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Get the message with the specified UID. - /// - /// - /// Gets the message with the specified UID. - /// - /// The message. - /// The UID of the message. - /// The cancellation token. - /// - /// is null. - /// - /// - /// is not a valid message UID. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The mail spool does not support UIDs. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The command failed. - /// - /// - /// A protocol error occurred. - /// - [Obsolete ("Use GetMessage (int index, CancellationToken cancellationToken = default (CancellationToken)) instead.")] - public abstract MimeMessage GetMessage (string uid, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously get the message with the specified UID. - /// - /// - /// Asynchronously gets the message with the specified UID. - /// - /// The message. - /// The UID of the message. - /// The cancellation token. - /// - /// is null. - /// - /// - /// is not a valid message UID. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The mail spool does not support UIDs. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The command failed. - /// - /// - /// A protocol error occurred. - /// - [Obsolete ("Use GetMessageAsync (int index, CancellationToken cancellationToken = default (CancellationToken)) instead.")] - public virtual Task GetMessageAsync (string uid, CancellationToken cancellationToken = default (CancellationToken)) - { - if (uid == null) - throw new ArgumentNullException (nameof (uid)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return GetMessage (uid, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Get the message at the specified index. - /// - /// - /// Gets the message at the specified index. + /// Gets the headers of the messages within the specified range. /// - /// - /// - /// - /// The message. - /// The index of the message. + /// The headers of the messages within the specified range. + /// The index of the first message to get. + /// The number of messages to get. /// The cancellation token. - /// The progress reporting mechanism. /// - /// is not a valid message index. + /// and do not specify + /// a valid range of messages. /// /// /// The has been disposed. @@ -1153,20 +636,21 @@ public abstract bool SupportsUids { /// /// A protocol error occurred. /// - public abstract MimeMessage GetMessage (int index, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + public abstract IList GetMessageHeaders (int startIndex, int count, CancellationToken cancellationToken = default); /// - /// Asynchronously get the message at the specified index. + /// Get the headers of the messages within the specified range. /// /// - /// Asynchronously gets the message at the specified index. + /// Gets the headers of the messages within the specified range. /// - /// The message. - /// The index of the message. + /// The headers of the messages within the specified range. + /// The index of the first message to get. + /// The number of messages to get. /// The cancellation token. - /// The progress reporting mechanism. /// - /// is not a valid message index. + /// and do not specify + /// a valid range of messages. /// /// /// The has been disposed. @@ -1189,31 +673,23 @@ public abstract bool SupportsUids { /// /// A protocol error occurred. /// - public virtual Task GetMessageAsync (int index, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) - { - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return GetMessage (index, cancellationToken, progress); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task> GetMessageHeadersAsync (int startIndex, int count, CancellationToken cancellationToken = default); /// - /// Get the messages with the specified UIDs. + /// Get the message at the specified index. /// /// - /// Gets the messages with the specified UIDs. + /// Gets the message at the specified index. /// - /// The messages. - /// The UIDs of the messages. + /// + /// + /// + /// The message. + /// The index of the message. /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the are invalid. - /// -or- - /// No uids were specified. + /// The progress reporting mechanism. + /// + /// is not a valid message index. /// /// /// The has been disposed. @@ -1224,9 +700,6 @@ public abstract bool SupportsUids { /// /// The is not authenticated. /// - /// - /// The mail spool does not support UIDs. - /// /// /// The operation was canceled via the cancellation token. /// @@ -1239,25 +712,20 @@ public abstract bool SupportsUids { /// /// A protocol error occurred. /// - [Obsolete ("Use GetMessages (IList indexes, CancellationToken cancellationToken = default (CancellationToken)) instead.")] - public abstract IList GetMessages (IList uids, CancellationToken cancellationToken = default (CancellationToken)); + public abstract MimeMessage GetMessage (int index, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Asynchronously get the messages with the specified UIDs. + /// Asynchronously get the message at the specified index. /// /// - /// Asynchronously gets the messages with the specified UIDs. + /// Asynchronously gets the message at the specified index. /// - /// The messages. - /// The UIDs of the messages. + /// The message. + /// The index of the message. /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the are invalid. - /// -or- - /// No uids were specified. + /// The progress reporting mechanism. + /// + /// is not a valid message index. /// /// /// The has been disposed. @@ -1268,9 +736,6 @@ public abstract bool SupportsUids { /// /// The is not authenticated. /// - /// - /// The mail spool does not support UIDs. - /// /// /// The operation was canceled via the cancellation token. /// @@ -1283,21 +748,7 @@ public abstract bool SupportsUids { /// /// A protocol error occurred. /// - [Obsolete ("Use GetMessagesAsync (IList indexes, CancellationToken cancellationToken = default (CancellationToken)) instead.")] - public virtual Task> GetMessagesAsync (IList uids, CancellationToken cancellationToken = default (CancellationToken)) - { - if (uids == null) - throw new ArgumentNullException (nameof (uids)); - - if (uids.Count == 0) - throw new ArgumentException ("No uids specified.", nameof (uids)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return GetMessages (uids, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task GetMessageAsync (int index, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// /// Get the messages at the specified indexes. @@ -1310,7 +761,7 @@ public abstract bool SupportsUids { /// The cancellation token. /// The progress reporting mechanism. /// - /// is null. + /// is . /// /// /// One or more of the are invalid. @@ -1338,7 +789,7 @@ public abstract bool SupportsUids { /// /// A protocol error occurred. /// - public abstract IList GetMessages (IList indexes, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + public abstract IList GetMessages (IList indexes, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// /// Asynchronously get the messages at the specified indexes. @@ -1351,7 +802,7 @@ public abstract bool SupportsUids { /// The cancellation token. /// The progress reporting mechanism. /// - /// is null. + /// is . /// /// /// One or more of the are invalid. @@ -1379,20 +830,7 @@ public abstract bool SupportsUids { /// /// A protocol error occurred. /// - public virtual Task> GetMessagesAsync (IList indexes, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) - { - if (indexes == null) - throw new ArgumentNullException (nameof (indexes)); - - if (indexes.Count == 0) - throw new ArgumentException ("No indexes specified.", nameof (indexes)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return GetMessages (indexes, cancellationToken, progress); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task> GetMessagesAsync (IList indexes, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// /// Get the messages within the specified range. @@ -1433,7 +871,7 @@ public abstract bool SupportsUids { /// /// A protocol error occurred. /// - public abstract IList GetMessages (int startIndex, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + public abstract IList GetMessages (int startIndex, int count, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// /// Asynchronously get the messages within the specified range. @@ -1471,14 +909,7 @@ public abstract bool SupportsUids { /// /// A protocol error occurred. /// - public virtual Task> GetMessagesAsync (int startIndex, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) - { - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return GetMessages (startIndex, count, cancellationToken, progress); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task> GetMessagesAsync (int startIndex, int count, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// /// Get the message or header stream at the specified index. @@ -1488,7 +919,7 @@ public abstract bool SupportsUids { /// /// The message or header stream. /// The index of the message. - /// true if only the headers should be retrieved; otherwise, false. + /// if only the headers should be retrieved; otherwise, . /// The cancellation token. /// The progress reporting mechanism. /// @@ -1515,7 +946,7 @@ public abstract bool SupportsUids { /// /// A protocol error occurred. /// - public abstract Stream GetStream (int index, bool headersOnly = false, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + public abstract Stream GetStream (int index, bool headersOnly = false, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// /// Asynchronously get the message or header stream at the specified index. @@ -1525,7 +956,7 @@ public abstract bool SupportsUids { /// /// The message or header stream. /// The index of the message. - /// true if only the headers should be retrieved; otherwise, false. + /// if only the headers should be retrieved; otherwise, . /// The cancellation token. /// The progress reporting mechanism. /// @@ -1552,14 +983,7 @@ public abstract bool SupportsUids { /// /// A protocol error occurred. /// - public virtual Task GetStreamAsync (int index, bool headersOnly = false, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) - { - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return GetStream (index, headersOnly, cancellationToken, progress); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task GetStreamAsync (int index, bool headersOnly = false, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// /// Get the message or header streams at the specified indexes. @@ -1572,11 +996,11 @@ public abstract bool SupportsUids { /// /// The message or header streams. /// The indexes of the messages. - /// true if only the headers should be retrieved; otherwise, false. + /// if only the headers should be retrieved; otherwise, . /// The cancellation token. /// The progress reporting mechanism. /// - /// is null. + /// is . /// /// /// One or more of the are invalid. @@ -1604,7 +1028,7 @@ public abstract bool SupportsUids { /// /// A protocol error occurred. /// - public abstract IList GetStreams (IList indexes, bool headersOnly = false, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + public abstract IList GetStreams (IList indexes, bool headersOnly = false, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// /// Asynchronously get the message or header streams at the specified indexes. @@ -1614,11 +1038,11 @@ public abstract bool SupportsUids { /// /// The messages. /// The indexes of the messages. - /// true if only the headers should be retrieved; otherwise, false. + /// if only the headers should be retrieved; otherwise, . /// The cancellation token. /// The progress reporting mechanism. /// - /// is null. + /// is . /// /// /// One or more of the are invalid. @@ -1646,20 +1070,7 @@ public abstract bool SupportsUids { /// /// A protocol error occurred. /// - public virtual Task> GetStreamsAsync (IList indexes, bool headersOnly = false, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) - { - if (indexes == null) - throw new ArgumentNullException (nameof (indexes)); - - if (indexes.Count == 0) - throw new ArgumentException ("No indexes specified.", nameof (indexes)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return GetStreams (indexes, headersOnly, cancellationToken, progress); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task> GetStreamsAsync (IList indexes, bool headersOnly = false, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// /// Get the message or header streams within the specified range. @@ -1673,7 +1084,7 @@ public abstract bool SupportsUids { /// The message or header streams. /// The index of the first stream to get. /// The number of streams to get. - /// true if only the headers should be retrieved; otherwise, false. + /// if only the headers should be retrieved; otherwise, . /// The cancellation token. /// The progress reporting mechanism. /// @@ -1701,7 +1112,7 @@ public abstract bool SupportsUids { /// /// A protocol error occurred. /// - public abstract IList GetStreams (int startIndex, int count, bool headersOnly = false, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + public abstract IList GetStreams (int startIndex, int count, bool headersOnly = false, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// /// Asynchronously get the message or header streams within the specified range. @@ -1712,7 +1123,7 @@ public abstract bool SupportsUids { /// The messages. /// The index of the first stream to get. /// The number of streams to get. - /// true if only the headers should be retrieved; otherwise, false. + /// if only the headers should be retrieved; otherwise, . /// The cancellation token. /// The progress reporting mechanism. /// @@ -1740,111 +1151,7 @@ public abstract bool SupportsUids { /// /// A protocol error occurred. /// - public virtual Task> GetStreamsAsync (int startIndex, int count, bool headersOnly = false, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) - { - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return GetStreams (startIndex, count, headersOnly, cancellationToken, progress); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Mark the specified message for deletion. - /// - /// - /// Messages marked for deletion are not actually deleted until the session - /// is cleanly disconnected - /// (see ). - /// - /// The UID of the message. - /// The cancellation token. - /// - /// is null. - /// - /// - /// is not a valid message UID. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The mail spool does not support UIDs. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The command failed. - /// - /// - /// A protocol error occurred. - /// - [Obsolete ("Use DeleteMessage (int index, CancellationToken cancellationToken = default (CancellationToken)) instead.")] - public abstract void DeleteMessage (string uid, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously mark the specified message for deletion. - /// - /// - /// Messages marked for deletion are not actually deleted until the session - /// is cleanly disconnected - /// (see ). - /// - /// An asynchronous task context. - /// The UID of the message. - /// The cancellation token. - /// - /// is null. - /// - /// - /// is not a valid message UID. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The mail spool does not support UIDs. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The command failed. - /// - /// - /// A protocol error occurred. - /// - [Obsolete ("Use DeleteMessageAsync (int index, CancellationToken cancellationToken = default (CancellationToken)) instead.")] - public virtual Task DeleteMessageAsync (string uid, CancellationToken cancellationToken = default (CancellationToken)) - { - if (uid == null) - throw new ArgumentNullException (nameof (uid)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - DeleteMessage (uid, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task> GetStreamsAsync (int startIndex, int count, bool headersOnly = false, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// /// Mark the specified message for deletion. @@ -1883,7 +1190,7 @@ public abstract bool SupportsUids { /// /// A protocol error occurred. /// - public abstract void DeleteMessage (int index, CancellationToken cancellationToken = default (CancellationToken)); + public abstract void DeleteMessage (int index, CancellationToken cancellationToken = default); /// /// Asynchronously mark the specified message for deletion. @@ -1920,118 +1227,7 @@ public abstract bool SupportsUids { /// /// A protocol error occurred. /// - public virtual Task DeleteMessageAsync (int index, CancellationToken cancellationToken = default (CancellationToken)) - { - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - DeleteMessage (index, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Mark the specified messages for deletion. - /// - /// - /// Messages marked for deletion are not actually deleted until the session - /// is cleanly disconnected - /// (see ). - /// - /// The UIDs of the messages. - /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the are invalid. - /// -or- - /// No uids were specified. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The mail spool does not support UIDs. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The command failed. - /// - /// - /// A protocol error occurred. - /// - [Obsolete ("Use DeleteMessages (IList indexes, CancellationToken cancellationToken = default (CancellationToken)) instead.")] - public abstract void DeleteMessages (IList uids, CancellationToken cancellationToken = default (CancellationToken)); - - /// - /// Asynchronously mark the specified messages for deletion. - /// - /// - /// Messages marked for deletion are not actually deleted until the session - /// is cleanly disconnected - /// (see ). - /// - /// An asynchronous task context. - /// The UIDs of the messages. - /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the are invalid. - /// -or- - /// No uids were specified. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The mail spool does not support UIDs. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The command failed. - /// - /// - /// A protocol error occurred. - /// - [Obsolete ("Use DeleteMessagesAsync (IList indexes, CancellationToken cancellationToken = default (CancellationToken)) instead.")] - public virtual Task DeleteMessagesAsync (IList uids, CancellationToken cancellationToken = default (CancellationToken)) - { - if (uids == null) - throw new ArgumentNullException (nameof (uids)); - - if (uids.Count == 0) - throw new ArgumentException ("No uids specified.", nameof (uids)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - DeleteMessages (uids, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task DeleteMessageAsync (int index, CancellationToken cancellationToken = default); /// /// Mark the specified messages for deletion. @@ -2044,7 +1240,7 @@ public abstract bool SupportsUids { /// The indexes of the messages. /// The cancellation token. /// - /// is null. + /// is . /// /// /// One or more of the are invalid. @@ -2072,7 +1268,7 @@ public abstract bool SupportsUids { /// /// A protocol error occurred. /// - public abstract void DeleteMessages (IList indexes, CancellationToken cancellationToken = default (CancellationToken)); + public abstract void DeleteMessages (IList indexes, CancellationToken cancellationToken = default); /// /// Asynchronously mark the specified messages for deletion. @@ -2086,7 +1282,7 @@ public abstract bool SupportsUids { /// The indexes of the messages. /// The cancellation token. /// - /// is null. + /// is . /// /// /// One or more of the are invalid. @@ -2114,20 +1310,7 @@ public abstract bool SupportsUids { /// /// A protocol error occurred. /// - public virtual Task DeleteMessagesAsync (IList indexes, CancellationToken cancellationToken = default (CancellationToken)) - { - if (indexes == null) - throw new ArgumentNullException (nameof (indexes)); - - if (indexes.Count == 0) - throw new ArgumentException ("No indexes specified.", nameof (indexes)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - DeleteMessages (indexes, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task DeleteMessagesAsync (IList indexes, CancellationToken cancellationToken = default); /// /// Mark the specified range of messages for deletion. @@ -2168,7 +1351,7 @@ public abstract bool SupportsUids { /// /// A protocol error occurred. /// - public abstract void DeleteMessages (int startIndex, int count, CancellationToken cancellationToken = default (CancellationToken)); + public abstract void DeleteMessages (int startIndex, int count, CancellationToken cancellationToken = default); /// /// Asynchronously mark the specified range of messages for deletion. @@ -2207,14 +1390,7 @@ public abstract bool SupportsUids { /// /// A protocol error occurred. /// - public virtual Task DeleteMessagesAsync (int startIndex, int count, CancellationToken cancellationToken = default (CancellationToken)) - { - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - DeleteMessages (startIndex, count, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task DeleteMessagesAsync (int startIndex, int count, CancellationToken cancellationToken = default); /// /// Mark all messages for deletion. @@ -2246,7 +1422,7 @@ public abstract bool SupportsUids { /// /// A protocol error occurred. /// - public abstract void DeleteAllMessages (CancellationToken cancellationToken = default (CancellationToken)); + public abstract void DeleteAllMessages (CancellationToken cancellationToken = default); /// /// Asynchronously mark all messages for deletion. @@ -2279,14 +1455,7 @@ public abstract bool SupportsUids { /// /// A protocol error occurred. /// - public virtual Task DeleteAllMessagesAsync (CancellationToken cancellationToken = default (CancellationToken)) - { - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - DeleteAllMessages (cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task DeleteAllMessagesAsync (CancellationToken cancellationToken = default); /// /// Reset the state of all messages marked for deletion. @@ -2318,7 +1487,7 @@ public abstract bool SupportsUids { /// /// A protocol error occurred. /// - public abstract void Reset (CancellationToken cancellationToken = default (CancellationToken)); + public abstract void Reset (CancellationToken cancellationToken = default); /// /// Asynchronously reset the state of all messages marked for deletion. @@ -2351,17 +1520,10 @@ public abstract bool SupportsUids { /// /// A protocol error occurred. /// - public Task ResetAsync (CancellationToken cancellationToken = default (CancellationToken)) - { - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - Reset (cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task ResetAsync (CancellationToken cancellationToken = default); /// - /// Gets an enumerator for the messages in the folder. + /// Get an enumerator for the messages in the folder. /// /// /// Gets an enumerator for the messages in the folder. @@ -2383,7 +1545,7 @@ public abstract bool SupportsUids { /// An I/O error occurred. /// /// - /// A POP3 command failed. + /// A command failed. /// /// /// A protocol error occurred. @@ -2391,7 +1553,7 @@ public abstract bool SupportsUids { public abstract IEnumerator GetEnumerator (); /// - /// Gets an enumerator for the messages in the folder. + /// Get an enumerator for the messages in the folder. /// /// /// Gets an enumerator for the messages in the folder. @@ -2413,7 +1575,7 @@ public abstract bool SupportsUids { /// An I/O error occurred. /// /// - /// A POP3 command failed. + /// A command failed. /// /// /// A protocol error occurred. diff --git a/MailKit/MailStore.cs b/MailKit/MailStore.cs index 0c10e8f5bf..eeb33d4081 100644 --- a/MailKit/MailStore.cs +++ b/MailKit/MailStore.cs @@ -1,9 +1,9 @@ -// +// // MailStore.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -46,7 +46,7 @@ public abstract class MailStore : MailService, IMailStore /// /// The protocol logger. /// - /// is null. + /// is . /// protected MailStore (IProtocolLogger protocolLogger) : base (protocolLogger) { @@ -91,11 +91,27 @@ public abstract FolderNamespaceCollection OtherNamespaces { /// /// Gets whether or not the mail store supports quotas. /// - /// true if the mail store supports quotas; otherwise, false. + /// if the mail store supports quotas; otherwise, . public abstract bool SupportsQuotas { get; } + /// + /// Get the threading algorithms supported by the mail store. + /// + /// + /// The threading algorithms are queried as part of the + /// Connect + /// and Authenticate methods. + /// + /// + /// + /// + /// The supported threading algorithms. + public abstract HashSet ThreadingAlgorithms { + get; + } + /// /// Get the Inbox folder. /// @@ -149,7 +165,7 @@ public abstract IMailFolder Inbox { /// /// The command failed. /// - public abstract void EnableQuickResync (CancellationToken cancellationToken = default (CancellationToken)); + public abstract void EnableQuickResync (CancellationToken cancellationToken = default); /// /// Asynchronously enable the quick resynchronization feature. @@ -193,14 +209,7 @@ public abstract IMailFolder Inbox { /// /// The command failed. /// - public virtual Task EnableQuickResyncAsync (CancellationToken cancellationToken = default (CancellationToken)) - { - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - EnableQuickResync (cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task EnableQuickResyncAsync (CancellationToken cancellationToken = default); /// /// Get the specified special folder. @@ -209,7 +218,7 @@ public abstract IMailFolder Inbox { /// Not all mail stores support special folders. Each implementation /// should provide a way to determine if special folders are supported. /// - /// The folder if available; otherwise null. + /// The folder if available; otherwise . /// The type of special folder. /// /// is out of range. @@ -223,7 +232,7 @@ public abstract IMailFolder Inbox { /// /// The is not authenticated. /// - public abstract IMailFolder GetFolder (SpecialFolder folder); + public abstract IMailFolder? GetFolder (SpecialFolder folder); /// /// Get the folder for the specified namespace. @@ -234,7 +243,7 @@ public abstract IMailFolder Inbox { /// The folder. /// The namespace. /// - /// is null. + /// is . /// /// /// The has been disposed. @@ -258,10 +267,10 @@ public abstract IMailFolder Inbox { /// /// The folders. /// The namespace. - /// If set to true, only subscribed folders will be listed. + /// If set to , only subscribed folders will be listed. /// The cancellation token. /// - /// is null. + /// is . /// /// /// The has been disposed. @@ -284,7 +293,7 @@ public abstract IMailFolder Inbox { /// /// The command failed. /// - public virtual IList GetFolders (FolderNamespace @namespace, bool subscribedOnly, CancellationToken cancellationToken = default (CancellationToken)) + public virtual IList GetFolders (FolderNamespace @namespace, bool subscribedOnly, CancellationToken cancellationToken = default) { return GetFolders (@namespace, StatusItems.None, subscribedOnly, cancellationToken); } @@ -297,10 +306,10 @@ public abstract IMailFolder Inbox { /// /// The folders. /// The namespace. - /// If set to true, only subscribed folders will be listed. + /// If set to , only subscribed folders will be listed. /// The cancellation token. /// - /// is null. + /// is . /// /// /// The has been disposed. @@ -323,16 +332,9 @@ public abstract IMailFolder Inbox { /// /// The command failed. /// - public virtual Task> GetFoldersAsync (FolderNamespace @namespace, bool subscribedOnly, CancellationToken cancellationToken = default (CancellationToken)) + public virtual Task> GetFoldersAsync (FolderNamespace @namespace, bool subscribedOnly, CancellationToken cancellationToken = default) { - if (@namespace == null) - throw new ArgumentNullException (nameof (@namespace)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return GetFolders (@namespace, subscribedOnly, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); + return GetFoldersAsync (@namespace, StatusItems.None, subscribedOnly, cancellationToken); } /// @@ -344,10 +346,10 @@ public abstract IMailFolder Inbox { /// The folders. /// The namespace. /// The status items to pre-populate. - /// If set to true, only subscribed folders will be listed. + /// If set to , only subscribed folders will be listed. /// The cancellation token. /// - /// is null. + /// is . /// /// /// The has been disposed. @@ -370,7 +372,7 @@ public abstract IMailFolder Inbox { /// /// The command failed. /// - public abstract IList GetFolders (FolderNamespace @namespace, StatusItems items = StatusItems.None, bool subscribedOnly = false, CancellationToken cancellationToken = default (CancellationToken)); + public abstract IList GetFolders (FolderNamespace @namespace, StatusItems items = StatusItems.None, bool subscribedOnly = false, CancellationToken cancellationToken = default); /// /// Asynchronously get all of the folders within the specified namespace. @@ -381,10 +383,10 @@ public abstract IMailFolder Inbox { /// The folders. /// The namespace. /// The status items to pre-populate. - /// If set to true, only subscribed folders will be listed. + /// If set to , only subscribed folders will be listed. /// The cancellation token. /// - /// is null. + /// is . /// /// /// The has been disposed. @@ -407,17 +409,7 @@ public abstract IMailFolder Inbox { /// /// The command failed. /// - public virtual Task> GetFoldersAsync (FolderNamespace @namespace, StatusItems items = StatusItems.None, bool subscribedOnly = false, CancellationToken cancellationToken = default (CancellationToken)) - { - if (@namespace == null) - throw new ArgumentNullException (nameof (@namespace)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return GetFolders (@namespace, items, subscribedOnly, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task> GetFoldersAsync (FolderNamespace @namespace, StatusItems items = StatusItems.None, bool subscribedOnly = false, CancellationToken cancellationToken = default); /// /// Get the folder for the specified path. @@ -429,7 +421,7 @@ public abstract IMailFolder Inbox { /// The folder path. /// The cancellation token. /// - /// is null. + /// is . /// /// /// The has been disposed. @@ -455,7 +447,7 @@ public abstract IMailFolder Inbox { /// /// The command failed. /// - public abstract IMailFolder GetFolder (string path, CancellationToken cancellationToken = default (CancellationToken)); + public abstract IMailFolder GetFolder (string path, CancellationToken cancellationToken = default); /// /// Asynchronously get the folder for the specified path. @@ -467,7 +459,7 @@ public abstract IMailFolder Inbox { /// The folder path. /// The cancellation token. /// - /// is null. + /// is . /// /// /// The has been disposed. @@ -493,17 +485,7 @@ public abstract IMailFolder Inbox { /// /// The command failed. /// - public virtual Task GetFolderAsync (string path, CancellationToken cancellationToken = default (CancellationToken)) - { - if (path == null) - throw new ArgumentNullException (nameof (path)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return GetFolder (path, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task GetFolderAsync (string path, CancellationToken cancellationToken = default); /// /// Gets the specified metadata. @@ -515,13 +497,13 @@ public abstract IMailFolder Inbox { /// The metadata tag. /// The cancellation token. /// - /// The has been disposed. + /// The has been disposed. /// /// - /// The is not connected. + /// The is not connected. /// /// - /// The is not authenticated. + /// The is not authenticated. /// /// /// The folder does not support metadata. @@ -538,7 +520,7 @@ public abstract IMailFolder Inbox { /// /// The command failed. /// - public abstract string GetMetadata (MetadataTag tag, CancellationToken cancellationToken = default (CancellationToken)); + public abstract string? GetMetadata (MetadataTag tag, CancellationToken cancellationToken = default); /// /// Asynchronously gets the specified metadata. @@ -550,13 +532,13 @@ public abstract IMailFolder Inbox { /// The metadata tag. /// The cancellation token. /// - /// The has been disposed. + /// The has been disposed. /// /// - /// The is not connected. + /// The is not connected. /// /// - /// The is not authenticated. + /// The is not authenticated. /// /// /// The folder does not support metadata. @@ -573,14 +555,7 @@ public abstract IMailFolder Inbox { /// /// The command failed. /// - public virtual Task GetMetadataAsync (MetadataTag tag, CancellationToken cancellationToken = default (CancellationToken)) - { - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return GetMetadata (tag, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task GetMetadataAsync (MetadataTag tag, CancellationToken cancellationToken = default); /// /// Gets the specified metadata. @@ -592,16 +567,16 @@ public abstract IMailFolder Inbox { /// The metadata tags. /// The cancellation token. /// - /// is null. + /// is . /// /// - /// The has been disposed. + /// The has been disposed. /// /// - /// The is not connected. + /// The is not connected. /// /// - /// The is not authenticated. + /// The is not authenticated. /// /// /// The folder does not support metadata. @@ -618,7 +593,7 @@ public abstract IMailFolder Inbox { /// /// The command failed. /// - public virtual MetadataCollection GetMetadata (IEnumerable tags, CancellationToken cancellationToken = default (CancellationToken)) + public virtual MetadataCollection GetMetadata (IEnumerable tags, CancellationToken cancellationToken = default) { return GetMetadata (new MetadataOptions (), tags, cancellationToken); } @@ -633,16 +608,16 @@ public abstract IMailFolder Inbox { /// The metadata tags. /// The cancellation token. /// - /// is null. + /// is . /// /// - /// The has been disposed. + /// The has been disposed. /// /// - /// The is not connected. + /// The is not connected. /// /// - /// The is not authenticated. + /// The is not authenticated. /// /// /// The folder does not support metadata. @@ -659,13 +634,9 @@ public abstract IMailFolder Inbox { /// /// The command failed. /// - public virtual Task GetMetadataAsync (IEnumerable tags, CancellationToken cancellationToken = default (CancellationToken)) + public virtual Task GetMetadataAsync (IEnumerable tags, CancellationToken cancellationToken = default) { - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return GetMetadata (tags, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); + return GetMetadataAsync (new MetadataOptions (), tags, cancellationToken); } /// @@ -679,18 +650,18 @@ public abstract IMailFolder Inbox { /// The metadata tags. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// - /// The has been disposed. + /// The has been disposed. /// /// - /// The is not connected. + /// The is not connected. /// /// - /// The is not authenticated. + /// The is not authenticated. /// /// /// The folder does not support metadata. @@ -707,7 +678,7 @@ public abstract IMailFolder Inbox { /// /// The command failed. /// - public abstract MetadataCollection GetMetadata (MetadataOptions options, IEnumerable tags, CancellationToken cancellationToken = default (CancellationToken)); + public abstract MetadataCollection GetMetadata (MetadataOptions options, IEnumerable tags, CancellationToken cancellationToken = default); /// /// Asynchronously gets the specified metadata. @@ -720,18 +691,18 @@ public abstract IMailFolder Inbox { /// The metadata tags. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// - /// The has been disposed. + /// The has been disposed. /// /// - /// The is not connected. + /// The is not connected. /// /// - /// The is not authenticated. + /// The is not authenticated. /// /// /// The folder does not support metadata. @@ -748,14 +719,7 @@ public abstract IMailFolder Inbox { /// /// The command failed. /// - public virtual Task GetMetadataAsync (MetadataOptions options, IEnumerable tags, CancellationToken cancellationToken = default (CancellationToken)) - { - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return GetMetadata (options, tags, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task GetMetadataAsync (MetadataOptions options, IEnumerable tags, CancellationToken cancellationToken = default); /// /// Sets the specified metadata. @@ -766,16 +730,16 @@ public abstract IMailFolder Inbox { /// The metadata. /// The cancellation token. /// - /// is null. + /// is . /// /// - /// The has been disposed. + /// The has been disposed. /// /// - /// The is not connected. + /// The is not connected. /// /// - /// The is not authenticated. + /// The is not authenticated. /// /// /// The folder does not support metadata. @@ -792,7 +756,7 @@ public abstract IMailFolder Inbox { /// /// The command failed. /// - public abstract void SetMetadata (MetadataCollection metadata, CancellationToken cancellationToken = default (CancellationToken)); + public abstract void SetMetadata (MetadataCollection metadata, CancellationToken cancellationToken = default); /// /// Asynchronously sets the specified metadata. @@ -804,16 +768,16 @@ public abstract IMailFolder Inbox { /// The metadata. /// The cancellation token. /// - /// is null. + /// is . /// /// - /// The has been disposed. + /// The has been disposed. /// /// - /// The is not connected. + /// The is not connected. /// /// - /// The is not authenticated. + /// The is not authenticated. /// /// /// The folder does not support metadata. @@ -830,14 +794,7 @@ public abstract IMailFolder Inbox { /// /// The command failed. /// - public virtual Task SetMetadataAsync (MetadataCollection metadata, CancellationToken cancellationToken = default (CancellationToken)) - { - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - SetMetadata (metadata, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task SetMetadataAsync (MetadataCollection metadata, CancellationToken cancellationToken = default); /// /// Occurs when a remote message store receives an alert message from the server. @@ -846,7 +803,7 @@ public abstract IMailFolder Inbox { /// The event is raised whenever the mail server sends an /// alert message. /// - public event EventHandler Alert; + public event EventHandler? Alert; /// /// Raise the alert event. @@ -856,14 +813,51 @@ public abstract IMailFolder Inbox { /// /// The alert message. /// - /// is null. + /// is . /// protected virtual void OnAlert (string message) { - var handler = Alert; + Alert?.Invoke (this, new AlertEventArgs (message)); + } + + /// + /// Occurs when a folder is created. + /// + /// + /// The event is emitted when a new folder is created. + /// + public event EventHandler? FolderCreated; - if (handler != null) - handler (this, new AlertEventArgs (message)); + /// + /// Raise the folder created event. + /// + /// + /// Raises the folder created event. + /// + /// The folder that was just created. + protected virtual void OnFolderCreated (IMailFolder folder) + { + FolderCreated?.Invoke (this, new FolderCreatedEventArgs (folder)); + } + + /// + /// Occurs when metadata changes. + /// + /// + /// The event is emitted when metadata changes. + /// + public event EventHandler? MetadataChanged; + + /// + /// Raise the metadata changed event. + /// + /// + /// Raises the metadata changed event. + /// + /// The metadata that changed. + protected virtual void OnMetadataChanged (Metadata metadata) + { + MetadataChanged?.Invoke (this, new MetadataChangedEventArgs (metadata)); } } } diff --git a/MailKit/MailTransport.cs b/MailKit/MailTransport.cs index 9950ae9919..39422a5b73 100644 --- a/MailKit/MailTransport.cs +++ b/MailKit/MailTransport.cs @@ -1,9 +1,9 @@ -// +// // MailTransport.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -40,6 +40,19 @@ namespace MailKit { /// public abstract class MailTransport : MailService, IMailTransport { + static readonly FormatOptions DefaultOptions; + + static MailTransport () + { + var options = FormatOptions.Default.Clone (); + options.HiddenHeaders.Add (HeaderId.ContentLength); + options.HiddenHeaders.Add (HeaderId.ResentBcc); + options.HiddenHeaders.Add (HeaderId.Bcc); + options.NewLineFormat = NewLineFormat.Dos; + + DefaultOptions = options; + } + /// /// Initializes a new instance of the class. /// @@ -48,14 +61,14 @@ public abstract class MailTransport : MailService, IMailTransport /// /// The protocol logger. /// - /// is null. + /// is . /// protected MailTransport (IProtocolLogger protocolLogger) : base (protocolLogger) { } /// - /// Sends the specified message. + /// Send the specified message. /// /// /// Sends the specified message. @@ -69,11 +82,12 @@ protected MailTransport (IProtocolLogger protocolLogger) : base (protocolLogger) /// /// /// + /// The final free-form text response from the server. /// The message. /// The cancellation token. /// The progress reporting mechanism. /// - /// is null. + /// is . /// /// /// The has been disposed. @@ -101,13 +115,13 @@ protected MailTransport (IProtocolLogger protocolLogger) : base (protocolLogger) /// /// A protocol exception occurred. /// - public virtual void Send (MimeMessage message, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + public virtual string Send (MimeMessage message, CancellationToken cancellationToken = default, ITransferProgress? progress = null) { - Send (FormatOptions.Default, message, cancellationToken, progress); + return Send (DefaultOptions, message, cancellationToken, progress); } /// - /// Asynchronously sends the specified message. + /// Asynchronously send the specified message. /// /// /// Asynchronously sends the specified message. @@ -118,12 +132,12 @@ protected MailTransport (IProtocolLogger protocolLogger) : base (protocolLogger) /// the recipients are collected from the Resent-To, Resent-Cc, and /// Resent-Bcc headers, otherwise the To, Cc, and Bcc headers are used. /// - /// An asynchronous task context. + /// The final free-form text response from the server. /// The message. /// The cancellation token. /// The progress reporting mechanism. /// - /// is null. + /// is . /// /// /// The has been disposed. @@ -151,35 +165,29 @@ protected MailTransport (IProtocolLogger protocolLogger) : base (protocolLogger) /// /// A protocol exception occurred. /// - public virtual Task SendAsync (MimeMessage message, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + public virtual Task SendAsync (MimeMessage message, CancellationToken cancellationToken = default, ITransferProgress? progress = null) { - if (message == null) - throw new ArgumentNullException (nameof (message)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - Send (message, cancellationToken, progress); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); + return SendAsync (DefaultOptions, message, cancellationToken, progress); } /// - /// Sends the specified message using the supplied sender and recipients. + /// Send the specified message using the supplied sender and recipients. /// /// /// Sends the specified message using the supplied sender and recipients. /// + /// The final free-form text response from the server. /// The message. /// The mailbox address to use for sending the message. /// The mailbox addresses that should receive the message. /// The cancellation token. /// The progress reporting mechanism. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// The has been disposed. @@ -207,29 +215,29 @@ protected MailTransport (IProtocolLogger protocolLogger) : base (protocolLogger) /// /// A protocol exception occurred. /// - public virtual void Send (MimeMessage message, MailboxAddress sender, IEnumerable recipients, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + public virtual string Send (MimeMessage message, MailboxAddress sender, IEnumerable recipients, CancellationToken cancellationToken = default, ITransferProgress? progress = null) { - Send (FormatOptions.Default, message, sender, recipients, cancellationToken, progress); + return Send (DefaultOptions, message, sender, recipients, cancellationToken, progress); } /// - /// Asynchronously sends the specified message using the supplied sender and recipients. + /// Asynchronously send the specified message using the supplied sender and recipients. /// /// /// Asynchronously sends the specified message using the supplied sender and recipients. /// - /// An asynchronous task context. + /// The final free-form text response from the server. /// The message. /// The mailbox address to use for sending the message. /// The mailbox addresses that should receive the message. /// The cancellation token. /// The progress reporting mechanism. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// The has been disposed. @@ -257,26 +265,13 @@ protected MailTransport (IProtocolLogger protocolLogger) : base (protocolLogger) /// /// A protocol exception occurred. /// - public virtual Task SendAsync (MimeMessage message, MailboxAddress sender, IEnumerable recipients, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + public virtual Task SendAsync (MimeMessage message, MailboxAddress sender, IEnumerable recipients, CancellationToken cancellationToken = default, ITransferProgress? progress = null) { - if (message == null) - throw new ArgumentNullException (nameof (message)); - - if (sender == null) - throw new ArgumentNullException (nameof (sender)); - - if (recipients == null) - throw new ArgumentNullException (nameof (recipients)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - Send (message, sender, recipients, cancellationToken, progress); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); + return SendAsync (DefaultOptions, message, sender, recipients, cancellationToken, progress); } /// - /// Sends the specified message. + /// Send the specified message. /// /// /// Sends the specified message. @@ -290,14 +285,15 @@ protected MailTransport (IProtocolLogger protocolLogger) : base (protocolLogger) /// /// /// + /// The final free-form text response from the server. /// The formatting options. /// The message. /// The cancellation token. /// The progress reporting mechanism. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// The has been disposed. @@ -328,10 +324,10 @@ protected MailTransport (IProtocolLogger protocolLogger) : base (protocolLogger) /// /// A protocol exception occurred. /// - public abstract void Send (FormatOptions options, MimeMessage message, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + public abstract string Send (FormatOptions options, MimeMessage message, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Asynchronously sends the specified message. + /// Asynchronously send the specified message. /// /// /// Asynchronously sends the specified message. @@ -342,15 +338,15 @@ protected MailTransport (IProtocolLogger protocolLogger) : base (protocolLogger) /// the recipients are collected from the Resent-To, Resent-Cc, and /// Resent-Bcc headers, otherwise the To, Cc, and Bcc headers are used. /// - /// An asynchronous task context. + /// The final free-form text response from the server. /// The formatting options. /// The message. /// The cancellation token. /// The progress reporting mechanism. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// The has been disposed. @@ -381,27 +377,15 @@ protected MailTransport (IProtocolLogger protocolLogger) : base (protocolLogger) /// /// A protocol exception occurred. /// - public virtual Task SendAsync (FormatOptions options, MimeMessage message, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) - { - if (options == null) - throw new ArgumentNullException (nameof (options)); - - if (message == null) - throw new ArgumentNullException (nameof (message)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - Send (options, message, cancellationToken, progress); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task SendAsync (FormatOptions options, MimeMessage message, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Sends the specified message using the supplied sender and recipients. + /// Send the specified message using the supplied sender and recipients. /// /// /// Sends the specified message using the supplied sender and recipients. /// + /// The final free-form text response from the server. /// The formatting options. /// The message. /// The mailbox address to use for sending the message. @@ -409,13 +393,13 @@ protected MailTransport (IProtocolLogger protocolLogger) : base (protocolLogger) /// The cancellation token. /// The progress reporting mechanism. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// -or- - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// The has been disposed. @@ -446,15 +430,15 @@ protected MailTransport (IProtocolLogger protocolLogger) : base (protocolLogger) /// /// A protocol exception occurred. /// - public abstract void Send (FormatOptions options, MimeMessage message, MailboxAddress sender, IEnumerable recipients, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + public abstract string Send (FormatOptions options, MimeMessage message, MailboxAddress sender, IEnumerable recipients, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// - /// Asynchronously sends the specified message using the supplied sender and recipients. + /// Asynchronously send the specified message using the supplied sender and recipients. /// /// /// Asynchronously sends the specified message using the supplied sender and recipients. /// - /// An asynchronous task context. + /// The final free-form text response from the server. /// The formatting options. /// The message. /// The mailbox address to use for sending the message. @@ -462,13 +446,13 @@ protected MailTransport (IProtocolLogger protocolLogger) : base (protocolLogger) /// The cancellation token. /// The progress reporting mechanism. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// -or- - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// The has been disposed. @@ -499,26 +483,7 @@ protected MailTransport (IProtocolLogger protocolLogger) : base (protocolLogger) /// /// A protocol exception occurred. /// - public virtual Task SendAsync (FormatOptions options, MimeMessage message, MailboxAddress sender, IEnumerable recipients, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) - { - if (options == null) - throw new ArgumentNullException (nameof (options)); - - if (message == null) - throw new ArgumentNullException (nameof (message)); - - if (sender == null) - throw new ArgumentNullException (nameof (sender)); - - if (recipients == null) - throw new ArgumentNullException (nameof (recipients)); - - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - Send (options, message, sender, recipients, cancellationToken, progress); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } + public abstract Task SendAsync (FormatOptions options, MimeMessage message, MailboxAddress sender, IEnumerable recipients, CancellationToken cancellationToken = default, ITransferProgress? progress = null); /// /// Occurs when a message is successfully sent via the transport. @@ -526,7 +491,7 @@ protected MailTransport (IProtocolLogger protocolLogger) : base (protocolLogger) /// /// The event will be emitted each time a message is successfully sent. /// - public event EventHandler MessageSent; + public event EventHandler? MessageSent; /// /// Raise the message sent event. @@ -537,10 +502,7 @@ protected MailTransport (IProtocolLogger protocolLogger) : base (protocolLogger) /// The message sent event args. protected virtual void OnMessageSent (MessageSentEventArgs e) { - var handler = MessageSent; - - if (handler != null) - handler (this, e); + MessageSent?.Invoke (this, e); } } } diff --git a/MailKit/MessageEventArgs.cs b/MailKit/MessageEventArgs.cs index 096f0ffc3f..441e92a128 100644 --- a/MailKit/MessageEventArgs.cs +++ b/MailKit/MessageEventArgs.cs @@ -1,9 +1,9 @@ -// +// // MessageEventArgs.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -53,6 +53,26 @@ public MessageEventArgs (int index) Index = index; } + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The message index. + /// The unique id of the message. + /// + /// is out of range. + /// + public MessageEventArgs (int index, UniqueId uid) + { + if (index < 0) + throw new ArgumentOutOfRangeException (nameof (index)); + + Index = index; + UniqueId = uid; + } + /// /// Gets the index of the message that changed. /// @@ -63,5 +83,16 @@ public MessageEventArgs (int index) public int Index { get; private set; } + + /// + /// Gets the unique ID of the message that changed, if available. + /// + /// + /// Gets the unique ID of the message that changed, if available. + /// + /// The unique ID of the message. + public UniqueId? UniqueId { + get; internal set; + } } } diff --git a/MailKit/MessageFlags.cs b/MailKit/MessageFlags.cs index e5ad588c77..70a4c63aee 100644 --- a/MailKit/MessageFlags.cs +++ b/MailKit/MessageFlags.cs @@ -1,9 +1,9 @@ -// +// // MessageFlags.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal diff --git a/MailKit/MessageFlagsChangedEventArgs.cs b/MailKit/MessageFlagsChangedEventArgs.cs index 2f9d7e252f..2254595a7b 100644 --- a/MailKit/MessageFlagsChangedEventArgs.cs +++ b/MailKit/MessageFlagsChangedEventArgs.cs @@ -1,9 +1,9 @@ -// +// // MessageFlagsChangedEventArgs.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -27,6 +27,12 @@ using System; using System.Collections.Generic; +#if NET5_0_OR_GREATER +using IReadOnlySetOfStrings = System.Collections.Generic.IReadOnlySet; +#else +using IReadOnlySetOfStrings = System.Collections.Generic.ISet; +#endif + namespace MailKit { /// /// Event args for the event. @@ -45,7 +51,7 @@ public class MessageFlagsChangedEventArgs : MessageEventArgs /// The message index. internal MessageFlagsChangedEventArgs (int index) : base (index) { - UserFlags = new HashSet (); + Keywords = new HashSet (StringComparer.Ordinal); } /// @@ -58,7 +64,7 @@ internal MessageFlagsChangedEventArgs (int index) : base (index) /// The message flags. public MessageFlagsChangedEventArgs (int index, MessageFlags flags) : base (index) { - UserFlags = new HashSet (); + Keywords = new HashSet (StringComparer.Ordinal); Flags = flags; } @@ -70,19 +76,19 @@ public MessageFlagsChangedEventArgs (int index, MessageFlags flags) : base (inde /// /// The message index. /// The message flags. - /// The user-defined message flags. + /// The user-defined keywords. /// - /// is null. + /// is . /// /// /// is out of range. /// - public MessageFlagsChangedEventArgs (int index, MessageFlags flags, HashSet userFlags) : base (index) + public MessageFlagsChangedEventArgs (int index, MessageFlags flags, IReadOnlySetOfStrings keywords) : base (index) { - if (userFlags == null) - throw new ArgumentNullException (nameof (userFlags)); + if (keywords == null) + throw new ArgumentNullException (nameof (keywords)); - UserFlags = userFlags; + Keywords = keywords; Flags = flags; } @@ -100,7 +106,7 @@ public MessageFlagsChangedEventArgs (int index, MessageFlags flags, HashSet public MessageFlagsChangedEventArgs (int index, MessageFlags flags, ulong modseq) : base (index) { - UserFlags = new HashSet (); + Keywords = new HashSet (StringComparer.Ordinal); ModSeq = modseq; Flags = flags; } @@ -113,20 +119,20 @@ public MessageFlagsChangedEventArgs (int index, MessageFlags flags, ulong modseq /// /// The message index. /// The message flags. - /// The user-defined message flags. + /// The user-defined keywords. /// The modification sequence value. /// - /// is null. + /// is . /// /// /// is out of range. /// - public MessageFlagsChangedEventArgs (int index, MessageFlags flags, HashSet userFlags, ulong modseq) : base (index) + public MessageFlagsChangedEventArgs (int index, MessageFlags flags, IReadOnlySetOfStrings keywords, ulong modseq) : base (index) { - if (userFlags == null) - throw new ArgumentNullException (nameof (userFlags)); + if (keywords == null) + throw new ArgumentNullException (nameof (keywords)); - UserFlags = userFlags; + Keywords = keywords; ModSeq = modseq; Flags = flags; } @@ -143,10 +149,9 @@ public MessageFlagsChangedEventArgs (int index, MessageFlags flags, HashSet /// is out of range. /// - public MessageFlagsChangedEventArgs (int index, UniqueId uid, MessageFlags flags) : base (index) + public MessageFlagsChangedEventArgs (int index, UniqueId uid, MessageFlags flags) : base (index, uid) { - UserFlags = new HashSet (); - UniqueId = uid; + Keywords = new HashSet (StringComparer.Ordinal); Flags = flags; } @@ -159,20 +164,19 @@ public MessageFlagsChangedEventArgs (int index, UniqueId uid, MessageFlags flags /// The message index. /// The unique id of the message. /// The message flags. - /// The user-defined message flags. + /// The user-defined keywords. /// - /// is null. + /// is . /// /// /// is out of range. /// - public MessageFlagsChangedEventArgs (int index, UniqueId uid, MessageFlags flags, HashSet userFlags) : base (index) + public MessageFlagsChangedEventArgs (int index, UniqueId uid, MessageFlags flags, IReadOnlySetOfStrings keywords) : base (index, uid) { - if (userFlags == null) - throw new ArgumentNullException (nameof (userFlags)); + if (keywords == null) + throw new ArgumentNullException (nameof (keywords)); - UserFlags = userFlags; - UniqueId = uid; + Keywords = keywords; Flags = flags; } @@ -189,11 +193,10 @@ public MessageFlagsChangedEventArgs (int index, UniqueId uid, MessageFlags flags /// /// is out of range. /// - public MessageFlagsChangedEventArgs (int index, UniqueId uid, MessageFlags flags, ulong modseq) : base (index) + public MessageFlagsChangedEventArgs (int index, UniqueId uid, MessageFlags flags, ulong modseq) : base (index, uid) { - UserFlags = new HashSet (); + Keywords = new HashSet (StringComparer.Ordinal); ModSeq = modseq; - UniqueId = uid; Flags = flags; } @@ -206,36 +209,24 @@ public MessageFlagsChangedEventArgs (int index, UniqueId uid, MessageFlags flags /// The message index. /// The unique id of the message. /// The message flags. - /// The user-defined message flags. + /// The user-defined message flags. /// The modification sequence value. /// - /// is null. + /// is . /// /// /// is out of range. /// - public MessageFlagsChangedEventArgs (int index, UniqueId uid, MessageFlags flags, HashSet userFlags, ulong modseq) : base (index) + public MessageFlagsChangedEventArgs (int index, UniqueId uid, MessageFlags flags, IReadOnlySetOfStrings keywords, ulong modseq) : base (index, uid) { - if (userFlags == null) - throw new ArgumentNullException (nameof (userFlags)); + if (keywords == null) + throw new ArgumentNullException (nameof (keywords)); - UserFlags = userFlags; + Keywords = keywords; ModSeq = modseq; - UniqueId = uid; Flags = flags; } - /// - /// Gets the unique ID of the message that changed, if available. - /// - /// - /// Gets the unique ID of the message that changed, if available. - /// - /// The unique ID of the message. - public UniqueId? UniqueId { - get; internal set; - } - /// /// Gets the updated message flags. /// @@ -254,8 +245,8 @@ public MessageFlags Flags { /// Gets the updated user-defined message flags. /// /// The updated user-defined message flags. - public HashSet UserFlags { - get; internal set; + public IReadOnlySetOfStrings Keywords { + get; private set; } /// diff --git a/MailKit/MessageLabelsChangedEventArgs.cs b/MailKit/MessageLabelsChangedEventArgs.cs index 92b56befe2..3eb0c31104 100644 --- a/MailKit/MessageLabelsChangedEventArgs.cs +++ b/MailKit/MessageLabelsChangedEventArgs.cs @@ -1,9 +1,9 @@ -// +// // LabelsChangedEventArgs.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -37,20 +37,6 @@ namespace MailKit { /// public class MessageLabelsChangedEventArgs : MessageEventArgs { - /// - /// Initializes a new instance of the class. - /// - /// - /// Creates a new . - /// - /// The message index. - /// - /// is out of range. - /// - internal MessageLabelsChangedEventArgs (int index) : base (index) - { - } - /// /// Initializes a new instance of the class. /// @@ -60,7 +46,7 @@ internal MessageLabelsChangedEventArgs (int index) : base (index) /// The message index. /// The message labels. /// - /// is null. + /// is . /// /// /// is out of range. @@ -83,7 +69,7 @@ public MessageLabelsChangedEventArgs (int index, IList labels) : base (i /// The message labels. /// The modification sequence value. /// - /// is null. + /// is . /// /// /// is out of range. @@ -107,18 +93,17 @@ public MessageLabelsChangedEventArgs (int index, IList labels, ulong mod /// The unique id of the message. /// The message labels. /// - /// is null. + /// is . /// /// /// is out of range. /// - public MessageLabelsChangedEventArgs (int index, UniqueId uid, IList labels) : base (index) + public MessageLabelsChangedEventArgs (int index, UniqueId uid, IList labels) : base (index, uid) { if (labels == null) throw new ArgumentNullException (nameof (labels)); Labels = new ReadOnlyCollection (labels); - UniqueId = uid; } /// @@ -132,32 +117,20 @@ public MessageLabelsChangedEventArgs (int index, UniqueId uid, IList lab /// The message labels. /// The modification sequence value. /// - /// is null. + /// is . /// /// /// is out of range. /// - public MessageLabelsChangedEventArgs (int index, UniqueId uid, IList labels, ulong modseq) : base (index) + public MessageLabelsChangedEventArgs (int index, UniqueId uid, IList labels, ulong modseq) : base (index, uid) { if (labels == null) throw new ArgumentNullException (nameof (labels)); Labels = new ReadOnlyCollection (labels); - UniqueId = uid; ModSeq = modseq; } - /// - /// Gets the unique ID of the message that changed, if available. - /// - /// - /// Gets the unique ID of the message that changed, if available. - /// - /// The unique ID of the message. - public UniqueId? UniqueId { - get; internal set; - } - /// /// Gets the updated labels. /// diff --git a/MailKit/MessageNotFoundException.cs b/MailKit/MessageNotFoundException.cs index 87a1a30b47..13901510e5 100644 --- a/MailKit/MessageNotFoundException.cs +++ b/MailKit/MessageNotFoundException.cs @@ -1,9 +1,9 @@ -// +// // MessageNotFoundException.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -56,9 +56,10 @@ public class MessageNotFoundException : Exception /// The serialization info. /// The streaming context. /// - /// is null. + /// is . /// [SecuritySafeCritical] + [Obsolete ("This API supports obsolete formatter-based serialization. It should not be called or extended by application code.")] protected MessageNotFoundException (SerializationInfo info, StreamingContext context) : base (info, context) { } diff --git a/MailKit/MessageSentEventArgs.cs b/MailKit/MessageSentEventArgs.cs index 35d4ea6456..feeb0d3e4c 100644 --- a/MailKit/MessageSentEventArgs.cs +++ b/MailKit/MessageSentEventArgs.cs @@ -1,9 +1,9 @@ -// +// // MessageSentEventArgs.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -46,9 +46,9 @@ public class MessageSentEventArgs : EventArgs /// The message that was just sent. /// The response from the server. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// public MessageSentEventArgs (MimeMessage message, string response) { diff --git a/MailKit/MessageSorter.cs b/MailKit/MessageSorter.cs index 10a5dd9561..58c3839cd6 100644 --- a/MailKit/MessageSorter.cs +++ b/MailKit/MessageSorter.cs @@ -1,9 +1,9 @@ -// +// // MessageSorter.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -93,50 +93,61 @@ static int CompareMailboxAddresses (InternetAddressList list1, InternetAddressLi return n1 ? 1 : (n2 ? -1 : 0); } - public int Compare (T x, T y) + public int Compare (T? x, T? y) { int cmp = 0; for (int i = 0; i < orderBy.Count; i++) { switch (orderBy[i].Type) { + case OrderByType.Annotation: + var annotation = (OrderByAnnotation) orderBy[i]; + + var xannotation = x!.Annotations?.FirstOrDefault (a => a.Entry == annotation.Entry); + var yannotation = y!.Annotations?.FirstOrDefault (a => a.Entry == annotation.Entry); + + var xvalue = xannotation?.Properties[annotation.Attribute] ?? string.Empty; + var yvalue = yannotation?.Properties[annotation.Attribute] ?? string.Empty; + + cmp = string.Compare (xvalue, yvalue, StringComparison.OrdinalIgnoreCase); + break; case OrderByType.Arrival: - cmp = x.Index.CompareTo (y.Index); + cmp = x!.Index.CompareTo (y!.Index); break; case OrderByType.Cc: - cmp = CompareMailboxAddresses (x.Envelope.Cc, y.Envelope.Cc); + cmp = CompareMailboxAddresses (x!.Envelope!.Cc, y!.Envelope!.Cc); break; case OrderByType.Date: - cmp = x.Date.CompareTo (y.Date); + cmp = x!.Date.CompareTo (y!.Date); break; case OrderByType.DisplayFrom: - cmp = CompareDisplayNames (x.Envelope.From, y.Envelope.From); + cmp = CompareDisplayNames (x!.Envelope!.From, y!.Envelope!.From); break; case OrderByType.From: - cmp = CompareMailboxAddresses (x.Envelope.From, y.Envelope.From); + cmp = CompareMailboxAddresses (x!.Envelope!.From, y!.Envelope!.From); + break; + case OrderByType.ModSeq: + var xmodseq = x!.ModSeq ?? 0; + var ymodseq = y!.ModSeq ?? 0; + + cmp = xmodseq.CompareTo (ymodseq); break; case OrderByType.Size: - var xsize = x.Size ?? 0; - var ysize = y.Size ?? 0; + var xsize = x!.Size ?? 0; + var ysize = y!.Size ?? 0; cmp = xsize.CompareTo (ysize); break; case OrderByType.Subject: - var xsubject = x.Envelope.Subject ?? string.Empty; - var ysubject = y.Envelope.Subject ?? string.Empty; + var xsubject = x!.Envelope!.Subject ?? string.Empty; + var ysubject = y!.Envelope!.Subject ?? string.Empty; cmp = string.Compare (xsubject, ysubject, StringComparison.OrdinalIgnoreCase); break; case OrderByType.DisplayTo: - cmp = CompareDisplayNames (x.Envelope.To, y.Envelope.To); + cmp = CompareDisplayNames (x!.Envelope!.To, y!.Envelope!.To); break; case OrderByType.To: - cmp = CompareMailboxAddresses (x.Envelope.To, y.Envelope.To); - break; - case OrderByType.ModSeq: - var xmodseq = x.ModSeq ?? 0; - var ymodseq = y.ModSeq ?? 0; - - cmp = xmodseq.CompareTo (ymodseq); + cmp = CompareMailboxAddresses (x!.Envelope!.To, y!.Envelope!.To); break; } @@ -158,6 +169,9 @@ static MessageSummaryItems GetMessageSummaryItems (IList orderBy) for (int i = 0; i < orderBy.Count; i++) { switch (orderBy[i].Type) { + case OrderByType.Annotation: + items |= MessageSummaryItems.Annotations; + break; case OrderByType.Arrival: break; case OrderByType.Cc: @@ -192,9 +206,9 @@ static MessageSummaryItems GetMessageSummaryItems (IList orderBy) /// The messages to sort. /// The sort ordering. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// contains one or more items that is missing information needed for sorting. @@ -243,9 +257,9 @@ public static IList Sort (this IEnumerable messages, IList ord /// The messages to sort. /// The sort ordering. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// contains one or more items that is missing information needed for sorting. diff --git a/MailKit/MessageSummary.cs b/MailKit/MessageSummary.cs index afdf3c3c77..e31a574955 100644 --- a/MailKit/MessageSummary.cs +++ b/MailKit/MessageSummary.cs @@ -1,9 +1,9 @@ -// +// // MessageSummary.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -27,24 +27,33 @@ using System; using System.Linq; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using MimeKit; using MimeKit.Utils; +#if NET5_0_OR_GREATER +using IReadOnlySetOfStrings = System.Collections.Generic.IReadOnlySet; +#else +using IReadOnlySetOfStrings = System.Collections.Generic.ISet; +#endif + namespace MailKit { /// /// A summary of a message. /// /// - /// A is returned by - /// . - /// The properties of the that will be available - /// depend on the passed to the aformentioned method. + /// The Fetch and + /// FetchAsync methods + /// return lists of items. + /// The properties of the that will be available + /// depend on the passed to the aforementioned method. /// public class MessageSummary : IMessageSummary { + IReadOnlySetOfStrings? keywords; int threadableReplyDepth = -1; - string normalizedSubject; + string? normalizedSubject; /// /// Initializes a new instance of the class. @@ -61,16 +70,38 @@ public MessageSummary (int index) if (index < 0) throw new ArgumentOutOfRangeException (nameof (index)); - UserFlags = new HashSet (); Index = index; } + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The folder that the message belongs to. + /// The message index. + /// + /// is . + /// + /// + /// is negative. + /// + public MessageSummary (IMailFolder folder, int index) : this (index) + { + if (folder == null) + throw new ArgumentNullException (nameof (folder)); + + Folder = folder; + } + + [MemberNotNull (nameof (normalizedSubject))] void UpdateThreadableSubject () { if (normalizedSubject != null) return; - if (Envelope.Subject != null) { + if (Envelope?.Subject != null) { normalizedSubject = MessageThreader.GetThreadableSubject (Envelope.Subject, out threadableReplyDepth); } else { normalizedSubject = string.Empty; @@ -78,6 +109,17 @@ void UpdateThreadableSubject () } } + /// + /// Get the folder that the message belongs to. + /// + /// + /// Gets the folder that the message belongs to, if available. + /// + /// The folder. + public IMailFolder? Folder { + get; private set; + } + /// /// Get a bitmask of fields that have been populated. /// @@ -96,20 +138,22 @@ public MessageSummaryItems Fields { /// The body will be one of , /// , , /// or . - /// This property will only be set if the - /// or - /// flag is used - /// when fetching summary information from a . + /// This property will only be set if either the + /// flag or the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. /// /// The body structure of the message. - public BodyPart Body { + public BodyPart? Body { get; set; } - static BodyPart GetMultipartRelatedRoot (BodyPartMultipart related) + static BodyPart? GetMultipartRelatedRoot (BodyPartMultipart related) { - string start = related.ContentType.Parameters["start"]; - string contentId; + string? start = related.ContentType.Parameters["start"]; + string? contentId; if (start == null) return related.BodyParts.Count > 0 ? related.BodyParts[0] : null; @@ -120,28 +164,23 @@ static BodyPart GetMultipartRelatedRoot (BodyPartMultipart related) var cid = new Uri (string.Format ("cid:{0}", contentId)); for (int i = 0; i < related.BodyParts.Count; i++) { - var basic = related.BodyParts[i] as BodyPartBasic; - - if (basic != null && (basic.ContentId == contentId || basic.ContentLocation == cid)) + if (related.BodyParts[i] is BodyPartBasic basic && (basic.ContentId == contentId || basic.ContentLocation == cid)) return basic; - var multipart = related.BodyParts[i] as BodyPartMultipart; - - if (multipart != null && multipart.ContentLocation == cid) + if (related.BodyParts[i] is BodyPartMultipart multipart && multipart.ContentLocation == cid) return multipart; } return null; } - static bool TryGetMultipartAlternativeBody (BodyPartMultipart multipart, bool html, out BodyPartText body) + static bool TryGetMultipartAlternativeBody (BodyPartMultipart multipart, bool html, [NotNullWhen (true)] out BodyPartText? body) { // walk the multipart/alternative children backwards from greatest level of faithfulness to the least faithful for (int i = multipart.BodyParts.Count - 1; i >= 0; i--) { - var multi = multipart.BodyParts[i] as BodyPartMultipart; - BodyPartText text = null; + BodyPartText? text = null; - if (multi != null) { + if (multipart.BodyParts[i] is BodyPartMultipart multi) { if (multi.ContentType.IsMimeType ("multipart", "related")) { text = GetMultipartRelatedRoot (multi) as BodyPartText; } else if (multi.ContentType.IsMimeType ("multipart", "alternative")) { @@ -164,10 +203,10 @@ static bool TryGetMultipartAlternativeBody (BodyPartMultipart multipart, bool ht return false; } - static bool TryGetMessageBody (BodyPartMultipart multipart, bool html, out BodyPartText body) + static bool TryGetMessageBody (BodyPartMultipart multipart, bool html, [NotNullWhen (true)] out BodyPartText? body) { - BodyPartMultipart multi; - BodyPartText text; + BodyPartMultipart? multi; + BodyPartText? text; if (multipart.ContentType.IsMimeType ("multipart", "alternative")) return TryGetMultipartAlternativeBody (multipart, html, out body); @@ -189,7 +228,7 @@ static bool TryGetMessageBody (BodyPartMultipart multipart, bool html, out BodyP text = multipart.BodyParts[i] as BodyPartText; // Look for the first non-attachment text part (realistically, the body text will - // preceed any attachments, but I'm not sure we can rely on that assumption). + // precede any attachments, but I'm not sure we can rely on that assumption). if (text != null && !text.IsAttachment) { if (html ? text.IsHtml : text.IsPlain) { body = text; @@ -229,27 +268,23 @@ static bool TryGetMessageBody (BodyPartMultipart multipart, bool html, out BodyP /// /// /// Gets the text/plain body part of the message. - /// In order for this to work properly, it is necessary to include - /// when fetching - /// summary information from a . + /// This property will only be usable if the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. /// /// - /// + /// /// - /// The text body if it exists; otherwise, null. - public BodyPartText TextBody { + /// The text body if it exists; otherwise, . + public BodyPartText? TextBody { get { - var multipart = Body as BodyPartMultipart; - - if (multipart != null) { - BodyPartText plain; - - if (TryGetMessageBody (multipart, false, out plain)) + if (Body is BodyPartMultipart multipart) { + if (TryGetMessageBody (multipart, false, out BodyPartText? plain)) return plain; } else { - var text = Body as BodyPartText; - - if (text != null && text.IsPlain) + if (Body is BodyPartText text && text.IsPlain) return text; } @@ -262,24 +297,23 @@ public BodyPartText TextBody { /// /// /// Gets the text/html body part of the message. - /// In order for this to work properly, it is necessary to include - /// when fetching - /// summary information from a . + /// This property will only be usable if the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. /// - /// The html body if it exists; otherwise, null. - public BodyPartText HtmlBody { + /// + /// + /// + /// The html body if it exists; otherwise, . + public BodyPartText? HtmlBody { get { - var multipart = Body as BodyPartMultipart; - - if (multipart != null) { - BodyPartText html; - - if (TryGetMessageBody (multipart, true, out html)) + if (Body is BodyPartMultipart multipart) { + if (TryGetMessageBody (multipart, true, out BodyPartText? html)) return html; } else { - var text = Body as BodyPartText; - - if (text != null && text.IsHtml) + if (Body is BodyPartText text && text.IsHtml) return text; } @@ -287,36 +321,26 @@ public BodyPartText HtmlBody { } } - static IEnumerable EnumerateBodyParts (BodyPart entity) + static IEnumerable EnumerateBodyParts (BodyPart? entity, bool attachmentsOnly) { if (entity == null) yield break; - var multipart = entity as BodyPartMultipart; - - if (multipart != null) { + if (entity is BodyPartMultipart multipart) { foreach (var subpart in multipart.BodyParts) { - foreach (var part in EnumerateBodyParts (subpart)) + foreach (var part in EnumerateBodyParts (subpart, attachmentsOnly)) yield return part; } yield break; } - var msgpart = entity as BodyPartMessage; - - if (msgpart != null) { - var message = msgpart.Body; - - if (message != null) { - foreach (var part in EnumerateBodyParts (message)) - yield return part; - } + var basic = (BodyPartBasic) entity; + if (attachmentsOnly && !basic.IsAttachment) yield break; - } - yield return (BodyPartBasic) entity; + yield return basic; } /// @@ -325,14 +349,16 @@ static IEnumerable EnumerateBodyParts (BodyPart entity) /// /// Traverses over the , enumerating all of the /// objects. - /// In order for this to work, it is necessary to include - /// or - /// when fetching - /// summary information from a . + /// This property will only be usable if either the + /// flag or the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. /// /// The body parts. public IEnumerable BodyParts { - get { return EnumerateBodyParts (Body); } + get { return EnumerateBodyParts (Body, false); } } /// @@ -342,16 +368,36 @@ public IEnumerable BodyParts { /// Traverses over the , enumerating all of the /// objects that have a Content-Disposition /// header set to "attachment". - /// In order for this to work properly, it is necessary to include - /// when fetching - /// summary information from a . + /// This property will only be usable if the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. /// /// - /// + /// /// /// The attachments. public IEnumerable Attachments { - get { return EnumerateBodyParts (Body).Where (part => part.IsAttachment); } + get { return EnumerateBodyParts (Body, true); } + } + + /// + /// Gets the preview text of the message. + /// + /// + /// The preview text is a short snippet of the beginning of the message + /// text, typically shown in a mail client's message list to provide the user + /// with a sense of what the message is about. + /// This property will only be set if the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. + /// + /// The preview text. + public string? PreviewText { + get; set; } /// @@ -365,21 +411,23 @@ public IEnumerable Attachments { /// and the message id. /// This property will only be set if the /// flag is passed to - /// . + /// one of the Fetch + /// or FetchAsync + /// methods. /// /// The envelope of the message. - public Envelope Envelope { + public Envelope? Envelope { get; set; } /// - /// Gets the threadable subject. + /// Gets the normalized subject. /// /// /// A normalized Subject header value where prefixes such as /// "Re:", "Re[#]:", etc have been pruned. /// - /// The threadable subject. + /// The normalized subject. public string NormalizedSubject { get { UpdateThreadableSubject (); @@ -394,7 +442,7 @@ public string NormalizedSubject { /// /// This value should be based on whether the message subject contained any "Re:" or "Fwd:" prefixes. /// - /// true if the message is a reply; otherwise, false. + /// if the message is a reply; otherwise, . public bool IsReply { get { UpdateThreadableSubject (); @@ -412,7 +460,7 @@ public bool IsReply { /// /// The date. public DateTimeOffset Date { - get { return Envelope.Date ?? InternalDate ?? DateTimeOffset.MinValue; } + get { return Envelope?.Date ?? InternalDate ?? DateTimeOffset.MinValue; } } /// @@ -422,7 +470,9 @@ public DateTimeOffset Date { /// Gets the message flags, if available. /// This property will only be set if the /// flag is passed to - /// . + /// one of the Fetch + /// or FetchAsync + /// methods. /// /// The message flags. public MessageFlags? Flags { @@ -436,10 +486,35 @@ public MessageFlags? Flags { /// Gets the user-defined message flags, if available. /// This property will only be set if the /// flag is passed to - /// . + /// one of the Fetch + /// or FetchAsync + /// methods. /// /// The user-defined message flags. - public HashSet UserFlags { + public IReadOnlySetOfStrings Keywords { + get { + keywords ??= new HashSet (StringComparer.Ordinal); + + return keywords; + } + set { + keywords = value; + } + } + + /// + /// Gets the message annotations, if available. + /// + /// + /// Gets the message annotations, if available. + /// This property will only be set if the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. + /// + /// The message annotations. + public IReadOnlyList? Annotations { get; set; } @@ -448,12 +523,14 @@ public HashSet UserFlags { /// /// /// Gets the list of headers, if available. - /// This property will only be set if the - /// . - /// method is used. + /// This property will only be set if the used with + /// Fetch or + /// FetchAsync has the + /// flag set on or if the list is non-empty. + /// /// /// The list of headers. - public HeaderList Headers { + public HeaderList? Headers { get; set; } @@ -464,13 +541,31 @@ public HeaderList Headers { /// Gets the internal date of the message (i.e. the "received" date), if available. /// This property will only be set if the /// flag is passed to - /// . + /// one of the Fetch + /// or FetchAsync + /// methods. /// /// The internal date of the message. public DateTimeOffset? InternalDate { get; set; } + /// + /// Gets the date and time that the message was saved to the current mailbox, if available. + /// + /// + /// Gets the date and time that the message was saved to the current mailbox, if available. + /// This property will only be set if the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. + /// + /// The save date of the message. + public DateTimeOffset? SaveDate { + get; set; + } + /// /// Gets the size of the message, in bytes, if available. /// @@ -478,7 +573,9 @@ public DateTimeOffset? InternalDate { /// Gets the size of the message, in bytes, if available. /// This property will only be set if the /// flag is passed to - /// . + /// one of the Fetch + /// or FetchAsync + /// methods. /// /// The size of the message. public uint? Size { @@ -492,7 +589,9 @@ public uint? Size { /// Gets the mod-sequence value for the message, if available. /// This property will only be set if the /// flag is passed to - /// . + /// one of the Fetch + /// or FetchAsync + /// methods. /// /// The mod-sequence value. public ulong? ModSeq { @@ -506,10 +605,48 @@ public ulong? ModSeq { /// Gets the message-ids that the message references, if available. /// This property will only be set if the /// flag is passed to - /// . + /// one of the Fetch + /// or FetchAsync + /// methods. /// /// The references. - public MessageIdList References { + public MessageIdList? References { + get; set; + } + + /// + /// Get the globally unique identifier for the message, if available. + /// + /// + /// Gets the globally unique identifier of the message, if available. + /// This property will only be set if the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. + /// This property maps to the EMAILID value defined in the + /// OBJECTID extension. + /// + /// The globally unique message identifier. + public string? EmailId { + get; set; + } + + /// + /// Get the globally unique thread identifier for the message, if available. + /// + /// + /// Gets the globally unique thread identifier for the message, if available. + /// This property will only be set if the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. + /// This property maps to the THREADID value defined in the + /// OBJECTID extension. + /// + /// The globally unique thread identifier. + public string? ThreadId { get; set; } @@ -520,7 +657,9 @@ public MessageIdList References { /// Gets the unique identifier of the message, if available. /// This property will only be set if the /// flag is passed to - /// . + /// one of the Fetch + /// or FetchAsync + /// methods. /// /// The uid of the message. public UniqueId UniqueId { @@ -531,11 +670,12 @@ public UniqueId UniqueId { /// Gets the index of the message. /// /// - /// Gets the index of the message. + /// Gets the index of the message. + /// This property is always set. /// /// The index of the message. public int Index { - get; private set; + get; internal set; } #region GMail extension properties @@ -547,7 +687,9 @@ public int Index { /// Gets the GMail message identifier, if available. /// This property will only be set if the /// flag is passed to - /// . + /// one of the Fetch + /// or FetchAsync + /// methods. /// /// The GMail message identifier. public ulong? GMailMessageId { @@ -561,7 +703,9 @@ public ulong? GMailMessageId { /// Gets the GMail thread identifier, if available. /// This property will only be set if the /// flag is passed to - /// . + /// one of the Fetch + /// or FetchAsync + /// methods. /// /// The GMail thread identifier. public ulong? GMailThreadId { @@ -575,10 +719,12 @@ public ulong? GMailThreadId { /// Gets the list of GMail labels, if available. /// This property will only be set if the /// flag is passed to - /// . + /// one of the Fetch + /// or FetchAsync + /// methods. /// /// The GMail labels. - public IList GMailLabels { + public IList? GMailLabels { get; set; } diff --git a/MailKit/MessageSummaryFetchedEventArgs.cs b/MailKit/MessageSummaryFetchedEventArgs.cs index 971e45eac0..248333d8ae 100644 --- a/MailKit/MessageSummaryFetchedEventArgs.cs +++ b/MailKit/MessageSummaryFetchedEventArgs.cs @@ -1,9 +1,9 @@ -// +// // MessageSummaryFetchedEventArgs.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -43,7 +43,7 @@ public class MessageSummaryFetchedEventArgs : EventArgs /// /// The message summary. /// - /// is null. + /// is . /// public MessageSummaryFetchedEventArgs (IMessageSummary message) { diff --git a/MailKit/MessageSummaryItems.cs b/MailKit/MessageSummaryItems.cs index e10e4b546f..0734473717 100644 --- a/MailKit/MessageSummaryItems.cs +++ b/MailKit/MessageSummaryItems.cs @@ -1,9 +1,9 @@ -// -// FetchFlags.cs +// +// MessageSummaryItems.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -33,9 +33,9 @@ namespace MailKit { /// /// are used to specify which properties /// of should be populated by calls to - /// , - /// , or - /// . + /// , + /// , or + /// . /// [Flags] public enum MessageSummaryItems { @@ -45,7 +45,16 @@ public enum MessageSummaryItems { None = 0, /// - /// Fetch the . + /// Fetch the . + /// Fetches all ANNOTATION values as defined in + /// rfc5257. + /// + Annotations = 1 << 0, + + /// + /// Fetch the . + /// Fetches the BODY value as defined in + /// rfc3501. /// Unlike , Body will not populate the /// parameters nor will it populate the /// , @@ -53,10 +62,12 @@ public enum MessageSummaryItems { /// body part. This makes Body far less useful than BodyStructure especially when /// it is desirable to determine whether or not a body part is an attachment. /// - Body = 1 << 0, + Body = 1 << 1, /// - /// Fetch the (but with more details than ). + /// Fetch the (but with more details than ). + /// Fetches the BODYSTRUCTURE value as defined in + /// rfc3501. /// Unlike , BodyStructure will also populate the /// parameters as well as the /// , @@ -67,83 +78,143 @@ public enum MessageSummaryItems { BodyStructure = 1 << 2, /// - /// Fetch the . + /// Fetch the . + /// Fetches the ENVELOPE value as defined in + /// rfc3501. /// Envelope = 1 << 3, /// - /// Fetch the . + /// Fetch the . + /// Fetches the FLAGS value as defined in + /// rfc3501. /// Flags = 1 << 4, /// - /// Fetch the . + /// Fetch the . + /// Fetches the INTERNALDATE value as defined in + /// rfc3501. /// InternalDate = 1 << 5, /// - /// Fetch the . + /// Fetch the . + /// Fetches the RFC822.SIZE value as defined in + /// rfc3501. /// Size = 1 << 6, /// - /// Fetch the . - /// - [Obsolete ("Use MessageSummaryItems.Size instead.")] - MessageSize = Size, - - /// - /// Fetch the . + /// Fetch the . + /// Fetches the MODSEQ value as defined in + /// rfc4551. /// ModSeq = 1 << 7, /// - /// Fetch the . + /// Fetch the . /// References = 1 << 8, /// - /// Fetch the . + /// Fetch the . + /// Fetches the UID value as defined in + /// rfc3501. /// UniqueId = 1 << 9, + /// + /// Fetch the . + /// Fetches the EMAILID value as defined in + /// rfc8474. + /// + EmailId = 1 << 10, + + /// + /// Fetch the . + /// Fetches the THREADID value as defined in + /// rfc8474. + /// + ThreadId = 1 << 11, + #region GMail extension items /// - /// Fetch the . + /// Fetch the . + /// Fetches the X-GM-MSGID value as defined in Google's + /// IMAP extensions + /// documentation. /// - GMailMessageId = 1 << 10, + GMailMessageId = 1 << 12, /// - /// Fetch the . + /// Fetch the . + /// Fetches the X-GM-THRID value as defined in Google's + /// IMAP extensions + /// documentation. /// - GMailThreadId = 1 << 11, + GMailThreadId = 1 << 13, /// - /// Fetch the . + /// Fetch the . + /// Fetches the X-GM-LABELS value as defined in Google's + /// IMAP extensions + /// documentation. /// - GMailLabels = 1 << 12, + GMailLabels = 1 << 14, #endregion + /// + /// Fetch the the complete list of for each message. + /// + Headers = 1 << 15, + + /// + /// Fetch the . + /// This property can be quite expensive to calculate because it is typically not an + /// item that is cached on the IMAP server. Instead, MailKit must download a hunk of the + /// message body so that it can decode and parse it in order to generate a meaningful + /// text snippet. This usually involves downloading the first 512 bytes for text/plain + /// message bodies and the first 16 kilobytes for text/html message bodies. If a + /// message contains both a text/plain body and a text/html body, then the + /// text/plain content is used in order to reduce network traffic. + /// + PreviewText = 1 << 16, + + /// + /// Fetch the . + /// Fetches the SAVEDATE value as defined in + /// rfc8514. + /// + SaveDate = 1 << 17, + #region Macros /// - /// A macro for , , , - /// and . + /// A macro for fetching the , , + /// , and values. + /// This macro maps to the equivalent ALL macro as defined in + /// rfc3501. /// - All = Envelope | Flags | InternalDate | Size, + All = Envelope | Flags | InternalDate | Size, /// - /// A macro for , , and . + /// A macro for fetching the , , and + /// values. + /// This macro maps to the equivalent FAST macro as defined in + /// rfc3501. /// - Fast = Flags | InternalDate | Size, + Fast = Flags | InternalDate | Size, /// - /// A macro for , , , - /// , and . + /// A macro for fetching the , , + /// , , and values. + /// This macro maps to the equivalent FULL macro as defined in + /// rfc3501. /// - Full = Body | Envelope | Flags| InternalDate | Size, + Full = Body | Envelope | Flags| InternalDate | Size, #endregion } diff --git a/MailKit/MessageThread.cs b/MailKit/MessageThread.cs index 1e0c810025..1c6168fba7 100644 --- a/MailKit/MessageThread.cs +++ b/MailKit/MessageThread.cs @@ -1,9 +1,9 @@ -// +// // MessageThread.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -33,7 +33,7 @@ namespace MailKit { /// /// A message thread. /// - public sealed class MessageThread + public class MessageThread { /// /// Initializes a new instance of the class. @@ -48,16 +48,49 @@ public MessageThread (UniqueId? uid) UniqueId = uid; } + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new message thread node. + /// + /// The message summary. + public MessageThread (IMessageSummary? message) + { + Children = new List (); + if (message != null && message.UniqueId.IsValid) + UniqueId = message.UniqueId; + Message = message; + } + + /// + /// Gets the message summary, if available. + /// + /// + /// Gets the message summary, if available. + /// This property will only ever be set if the + /// was created by the . s that are + /// created by any of the + /// Thread or + /// ThreadAsync + /// methods will always be . + /// + /// The message summary. + public IMessageSummary? Message { + get; private set; + } + /// /// Gets the unique identifier of the message. /// /// - /// The unique identifier may be null if the message is missing - /// from the or did not match the - /// . + /// The unique identifier may be if the message is missing from the + /// or from the list of messages provided to the + /// . /// /// The unique identifier. public UniqueId? UniqueId { + // FIXME: this shouldn't be a nullable since we can just use UniqueId.Invalid get; private set; } diff --git a/MailKit/MessageThreader.cs b/MailKit/MessageThreader.cs index 80d46d592d..97eb3fa30c 100644 --- a/MailKit/MessageThreader.cs +++ b/MailKit/MessageThreader.cs @@ -1,9 +1,9 @@ -// +// // MessageThreader.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -27,12 +27,19 @@ using System; using System.Text; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using MimeKit; using MimeKit.Utils; using MailKit.Search; +#if NET5_0_OR_GREATER +using IReadOnlySetOfStrings = System.Collections.Generic.IReadOnlySet; +#else +using IReadOnlySetOfStrings = System.Collections.Generic.ISet; +#endif + namespace MailKit { /// /// Threads messages according to the algorithms defined in rfc5256. @@ -42,12 +49,18 @@ namespace MailKit { /// public static class MessageThreader { - class ThreadableNode : IMessageSummary + internal class ThreadableNode : IMessageSummary { public readonly List Children = new List (); - public IMessageSummary Message; - public ThreadableNode Parent; + public IMessageSummary? Message; + public ThreadableNode? Parent; + + public ThreadableNode (IMessageSummary? message) + { + Message = message; + } + [MemberNotNullWhen (true, nameof (Parent))] public bool HasParent { get { return Parent != null; } } @@ -56,31 +69,25 @@ public bool HasChildren { get { return Children.Count > 0; } } + public IMailFolder? Folder => null; + public MessageSummaryItems Fields { - get { return MessageSummaryItems.UniqueId | MessageSummaryItems.Envelope; } + get { return MessageSummaryItems.UniqueId | MessageSummaryItems.Envelope | MessageSummaryItems.ModSeq | MessageSummaryItems.Size; } } - public BodyPart Body { - get { return null; } - } + public BodyPart? Body => null; - public BodyPartText TextBody { - get { return null; } - } + public BodyPartText? TextBody => null; - public BodyPartText HtmlBody { - get { return null; } - } + public BodyPartText? HtmlBody => null; - public IEnumerable BodyParts { - get { yield break; } - } + public IEnumerable BodyParts => Array.Empty (); - public IEnumerable Attachments { - get { yield break; } - } + public IEnumerable Attachments => Array.Empty (); - public Envelope Envelope { + public string? PreviewText => null; + + public Envelope? Envelope { get { return Message != null ? Message.Envelope : Children[0].Envelope; } } @@ -96,21 +103,21 @@ public bool IsReply { get { return Message != null && Message.IsReply; } } - public MessageFlags? Flags { - get { return Message != null ? Message.Flags : Children[0].Flags; } - } + public MessageFlags? Flags => null; - public HashSet UserFlags { - get { return Message != null ? Message.UserFlags : Children[0].UserFlags; } + public IReadOnlySetOfStrings Keywords { + get { return new HashSet (); } } - public HeaderList Headers { - get { return Message != null ? Message.Headers : Children[0].Headers; } + public IReadOnlyList? Annotations { + get { return Message != null ? Message.Annotations : Children[0].Annotations; } } - public DateTimeOffset? InternalDate { - get { return Message != null ? Message.InternalDate : Children[0].InternalDate; } - } + public HeaderList? Headers => null; + + public DateTimeOffset? InternalDate => null; + + public DateTimeOffset? SaveDate => null; public uint? Size { get { return Message != null ? Message.Size : Children[0].Size; } @@ -120,10 +127,14 @@ public ulong? ModSeq { get { return Message != null ? Message.ModSeq : Children[0].ModSeq; } } - public MessageIdList References { + public MessageIdList? References { get { return Message != null ? Message.References : Children[0].References; } } + public string? EmailId => null; + + public string? ThreadId => null; + public UniqueId UniqueId { get { return Message != null ? Message.UniqueId : Children[0].UniqueId; } } @@ -132,23 +143,16 @@ public int Index { get { return Message != null ? Message.Index : Children[0].Index; } } - public ulong? GMailMessageId { - get { return Message != null ? Message.GMailMessageId : Children[0].GMailMessageId; } - } + public ulong? GMailMessageId => null; - public ulong? GMailThreadId { - get { return Message != null ? Message.GMailThreadId : Children[0].GMailThreadId; } - } + public ulong? GMailThreadId => null; - public IList GMailLabels { - get { return Message != null ? Message.GMailLabels : Children[0].GMailLabels; } - } + public IList? GMailLabels => null; } - static IDictionary CreateIdTable (IEnumerable messages) + static Dictionary CreateIdTable (IEnumerable messages) { - var ids = new Dictionary (); - ThreadableNode node; + var ids = new Dictionary (StringComparer.OrdinalIgnoreCase); foreach (var message in messages) { if (message.Envelope == null) @@ -159,7 +163,7 @@ static IDictionary CreateIdTable (IEnumerable CreateIdTable (IEnumerable CreateIdTable (IEnumerable ids) { - var root = new ThreadableNode (); + var root = new ThreadableNode (null); foreach (var message in ids.Values) { if (message.Parent == null) @@ -261,7 +265,7 @@ static void PruneEmptyContainers (ThreadableNode root) static void GroupBySubject (ThreadableNode root) { var subjects = new Dictionary (StringComparer.OrdinalIgnoreCase); - ThreadableNode match; + ThreadableNode? match; int count = 0; for (int i = 0; i < root.Children.Count; i++) { @@ -311,7 +315,7 @@ static void GroupBySubject (ThreadableNode root) // is not, make the current message a child of the message in the subject // table (a sibling of its children). match.Children.Add (current); - } else if (current.Message.IsReply && !match.Message.IsReply) { + } else if (current.IsReply && !match.IsReply) { // If the current message is a reply or forward and the message in the // subject table is not, make the current message a child of the message // in the subject table (a sibling of its children). @@ -326,8 +330,7 @@ static void GroupBySubject (ThreadableNode root) var dummy = match; // clone the message already in the subject table - match = new ThreadableNode (); - match.Message = dummy.Message; + match = new ThreadableNode (dummy.Message); match.Children.AddRange (dummy.Children); // empty out the old match node (aka the new dummy node) @@ -347,18 +350,14 @@ static void GetThreads (ThreadableNode root, IList threads, IList for (int i = 0; i < root.Children.Count; i++) { var message = root.Children[i].Message; - UniqueId? uid = null; - - if (message != null) - uid = message.UniqueId; + var thread = new MessageThread (message); - var thread = new MessageThread (uid); GetThreads (root.Children[i], thread.Children, orderBy); threads.Add (thread); } } - static IList ThreadByReferences (IEnumerable messages, IList orderBy) + static List ThreadByReferences (IEnumerable messages, IList orderBy) { var threads = new List (); var ids = CreateIdTable (messages); @@ -372,19 +371,18 @@ static IList ThreadByReferences (IEnumerable mes return threads; } - static IList ThreadBySubject (IEnumerable messages, IList orderBy) + static List ThreadBySubject (IEnumerable messages, IList orderBy) { var threads = new List (); - var root = new ThreadableNode (); + var root = new ThreadableNode (null); foreach (var message in messages) { if (message.Envelope == null) throw new ArgumentException ("One or more messages is missing information needed for threading.", nameof (messages)); - var container = new ThreadableNode (); - container.Message = message; + var node = new ThreadableNode (message); - root.Children.Add (container); + root.Children.Add (node); } GroupBySubject (root); @@ -404,7 +402,7 @@ static IList ThreadBySubject (IEnumerable messag /// The messages. /// The threading algorithm. /// - /// is null. + /// is . /// /// /// is not a valid threading algorithm. @@ -430,9 +428,9 @@ public static IList Thread (this IEnumerable mes /// The threading algorithm. /// The requested sort ordering. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// is not a valid threading algorithm. @@ -515,7 +513,7 @@ static bool SkipDigits (string subject, ref int index, out int value) /// The Subject header value. /// The reply depth. /// - /// is null. + /// is . /// public static string GetThreadableSubject (string subject, out int replyDepth) { @@ -526,8 +524,7 @@ public static string GetThreadableSubject (string subject, out int replyDepth) int endIndex = subject.Length; int startIndex = 0; - int index, count; - int left; + int index, left; do { SkipWhiteSpace (subject, ref startIndex); @@ -558,7 +555,7 @@ public static string GetThreadableSubject (string subject, out int replyDepth) index += 3; // if this is followed by "###]:" or "###):", then it's a condensed "Re:" - if (SkipDigits (subject, ref index, out count) && (endIndex - index) >= 2 && + if (SkipDigits (subject, ref index, out int count) && (endIndex - index) >= 2 && subject[index] == close && subject[index + 1] == ':') { startIndex = index + 2; replyDepth += count; @@ -602,7 +599,7 @@ public static string GetThreadableSubject (string subject, out int replyDepth) var canonicalized = builder.ToString (); - if (canonicalized.ToLowerInvariant () == "(no subject)") + if (canonicalized.Equals ("(no subject)", StringComparison.OrdinalIgnoreCase)) canonicalized = string.Empty; return canonicalized; diff --git a/MailKit/MessagesVanishedEventArgs.cs b/MailKit/MessagesVanishedEventArgs.cs index cf621b81d6..0c99932770 100644 --- a/MailKit/MessagesVanishedEventArgs.cs +++ b/MailKit/MessagesVanishedEventArgs.cs @@ -3,7 +3,7 @@ // // Author: Jeffrey Stedfast // -// Copyright (c) 2014 Jeffrey Stedfast +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -44,9 +44,9 @@ public class MessagesVanishedEventArgs : EventArgs /// Creates a new . /// /// The list of unique identifiers. - /// If set to true, the messages vanished in the past as opposed to just now. + /// If set to , the messages vanished in the past as opposed to just now. /// - /// is null. + /// is . /// public MessagesVanishedEventArgs (IList uids, bool earlier) { @@ -66,12 +66,12 @@ public IList UniqueIds { } /// - /// Gets whether the messages vanished inthe past as opposed to just now. + /// Gets whether the messages vanished in the past as opposed to just now. /// /// - /// Gets whether the messages vanished inthe past as opposed to just now. + /// Gets whether the messages vanished in the past as opposed to just now. /// - /// true if the messages vanished earlier; otherwise, false. + /// if the messages vanished earlier; otherwise, . public bool Earlier { get; private set; } diff --git a/MailKit/Metadata.cs b/MailKit/Metadata.cs index 26e0071228..521fc4ee53 100644 --- a/MailKit/Metadata.cs +++ b/MailKit/Metadata.cs @@ -1,9 +1,9 @@ -// +// // Metadata.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -33,6 +33,8 @@ namespace MailKit { /// public class Metadata { + internal string EncodedName; + /// /// Initializes a new instance of the class. /// @@ -40,9 +42,10 @@ public class Metadata /// Creates a new . /// /// The metadata tag. - /// The meatdata value. + /// The metadata value. public Metadata (MetadataTag tag, string value) { + EncodedName = string.Empty; Value = value; Tag = tag; } diff --git a/MailKit/MetadataChangedEventArgs.cs b/MailKit/MetadataChangedEventArgs.cs new file mode 100644 index 0000000000..a4cd099dc0 --- /dev/null +++ b/MailKit/MetadataChangedEventArgs.cs @@ -0,0 +1,67 @@ +// +// MetadataChangedEventArgs.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MailKit { + /// + /// Event args used when a metadata changes. + /// + /// + /// Event args used when a metadata changes. + /// + public class MetadataChangedEventArgs : EventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The metadata that changed. + /// + /// is . + /// + public MetadataChangedEventArgs (Metadata metadata) + { + if (metadata == null) + throw new ArgumentNullException (nameof (metadata)); + + Metadata = metadata; + } + + /// + /// Get the metadata that changed. + /// + /// + /// Gets the metadata that changed. + /// + /// The metadata. + public Metadata Metadata { + get; private set; + } + } +} diff --git a/MailKit/MetadataCollection.cs b/MailKit/MetadataCollection.cs index 83a69e5813..1b2479a4b4 100644 --- a/MailKit/MetadataCollection.cs +++ b/MailKit/MetadataCollection.cs @@ -1,9 +1,9 @@ -// +// // MetadataCollection.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal diff --git a/MailKit/MetadataOptions.cs b/MailKit/MetadataOptions.cs index a6cbd3ac82..c7967d0d90 100644 --- a/MailKit/MetadataOptions.cs +++ b/MailKit/MetadataOptions.cs @@ -1,9 +1,9 @@ -// +// // MetadataOptions.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal diff --git a/MailKit/MetadataTag.cs b/MailKit/MetadataTag.cs index 0b0203d7e7..de0b338222 100644 --- a/MailKit/MetadataTag.cs +++ b/MailKit/MetadataTag.cs @@ -1,9 +1,9 @@ -// +// // MetadataTag.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -36,7 +36,7 @@ namespace MailKit { public struct MetadataTag { /// - /// Indicates a method for contacting the server administrator. + /// A metadata tag for specifying the contact information for the server administrator. /// /// /// Used to get the contact information of the administrator on a @@ -45,7 +45,7 @@ public struct MetadataTag public static readonly MetadataTag SharedAdmin = new MetadataTag ("/shared/admin"); /// - /// Indicates a private comment. + /// A metadata tag for private comments. /// /// /// Used to get or set a private comment on a . @@ -53,7 +53,7 @@ public struct MetadataTag public static readonly MetadataTag PrivateComment = new MetadataTag ("/private/comment"); /// - /// Indicates a shared comment. + /// A metadata tag for shared comments. /// /// /// Used to get or set a shared comment on a @@ -62,15 +62,13 @@ public struct MetadataTag public static readonly MetadataTag SharedComment = new MetadataTag ("/shared/comment"); /// - /// Indicates a method for specifying the special use for a particular folder. + /// A metadata tag for specifying the special use of a folder. /// /// /// Used to get or set the special use of a . /// public static readonly MetadataTag PrivateSpecialUse = new MetadataTag ("/private/specialuse"); - readonly string id; - /// /// Initializes a new instance of the struct. /// @@ -79,7 +77,7 @@ public struct MetadataTag /// /// The metadata tag identifier. /// - /// is null. + /// is . /// /// /// is an empty string. @@ -92,32 +90,32 @@ public MetadataTag (string id) if (id.Length == 0) throw new ArgumentException ("A metadata tag identifier cannot be empty."); - this.id = id; + Id = id; } /// - /// Gets the metadata tag identifier. + /// Get the metadata tag identifier. /// /// /// Gets the metadata tag identifier. /// /// The metadata tag identifier. public string Id { - get { return id; } + get; private set; } /// - /// Determines whether the specified is equal to the current . + /// Determine whether the specified is equal to the current . /// /// /// Determines whether the specified is equal to the current . /// /// The to compare with the current . - /// true if the specified is equal to the current - /// ; otherwise, false. - public override bool Equals (object obj) + /// if the specified is equal to the current + /// ; otherwise, . + public override bool Equals (object? obj) { - return Id.Equals (obj); + return obj is MetadataTag tag && tag.Id == Id; } /// diff --git a/MailKit/ModSeqChangedEventArgs.cs b/MailKit/ModSeqChangedEventArgs.cs index 35da57fdb6..5451baa7e9 100644 --- a/MailKit/ModSeqChangedEventArgs.cs +++ b/MailKit/ModSeqChangedEventArgs.cs @@ -1,9 +1,9 @@ -// +// // ModSeqChangedEventArgs.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Jeffrey Stedfast +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -34,17 +34,6 @@ namespace MailKit /// public class ModSeqChangedEventArgs : MessageEventArgs { - /// - /// Initializes a new instance of the class. - /// - /// - /// Creates a new . - /// - /// The message index. - internal ModSeqChangedEventArgs (int index) : base (index) - { - } - /// /// Initializes a new instance of the class. /// @@ -67,21 +56,9 @@ public ModSeqChangedEventArgs (int index, ulong modseq) : base (index) /// The message index. /// The unique id of the message. /// The modification sequence value. - public ModSeqChangedEventArgs (int index, UniqueId uid, ulong modseq) : base (index) + public ModSeqChangedEventArgs (int index, UniqueId uid, ulong modseq) : base (index, uid) { ModSeq = modseq; - UniqueId = uid; - } - - /// - /// Gets the unique ID of the message that changed, if available. - /// - /// - /// Gets the unique ID of the message that changed, if available. - /// - /// The unique ID of the message. - public UniqueId? UniqueId { - get; internal set; } /// @@ -92,7 +69,7 @@ public UniqueId? UniqueId { /// /// The mod-sequence value. public ulong ModSeq { - get; internal set; + get; private set; } } } diff --git a/MailKit/Net/ClientMetrics.cs b/MailKit/Net/ClientMetrics.cs new file mode 100644 index 0000000000..c94891e887 --- /dev/null +++ b/MailKit/Net/ClientMetrics.cs @@ -0,0 +1,131 @@ +// +// ClientMetrics.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +#if NET6_0_OR_GREATER + +using System; +using System.Diagnostics; +using System.Globalization; +using System.Diagnostics.Metrics; +using System.Diagnostics.CodeAnalysis; + +using MailKit.Net.Smtp; +using MailKit.Security; + +namespace MailKit.Net { + sealed class ClientMetrics + { + public readonly Histogram ConnectionDuration; + public readonly Counter OperationCounter; + public readonly Histogram OperationDuration; + public readonly string MeterName; + + public ClientMetrics (Meter meter, string meterName, string an, string protocol) + { + MeterName = meterName; + + ConnectionDuration = meter.CreateHistogram ( + name: $"{meterName}.client.connection.duration", + unit: "s", + description: $"The duration of successfully established connections to {an} {protocol} server."); + + OperationCounter = meter.CreateCounter ( + name: $"{meterName}.client.operation.count", + unit: "{operation}", + description: $"The number of times a client performed an operation on {an} {protocol} server."); + + OperationDuration = meter.CreateHistogram ( + name: $"{meterName}.client.operation.duration", + unit: "ms", + description: $"The amount of time it takes for the {protocol} server to perform an operation."); + } + + static bool TryGetErrorType (Exception exception, [NotNullWhen (true)] out string? errorType) + { + if (SocketMetrics.TryGetErrorType (exception, false, out errorType)) + return true; + + if (exception is SslHandshakeException) { + // Note: The string "secure_connection_error" is used by HttpClient for SSL/TLS handshake errors. + errorType = "secure_connection_error"; + return true; + } + + if (exception is ProtocolException) { + // TODO: ProtocolExceptions tend to be either "Unexpectedly disconnected" or "Parse error". + // If we add a property to ProtocolException to tell us this, we could report it better here. + // + // To mimic HttpClient error.type values, we could use "response_ended" and "invalid_response", respectively. + // + // Alternatively, HttpClient also uses "http_protocol_error" so we could use "smtp/pop3/imap_protocol_error". + errorType = "protocol_error"; + return true; + } + + if (exception is SmtpCommandException smtp) { + errorType = ((int) smtp.StatusCode).ToString (CultureInfo.InvariantCulture); + return true; + } + + if (exception is CommandException) { + // FIXME: We need to add a property to CommandException to tell us the error type. + errorType = "command_error"; + return true; + } + + // Fall back to using the exception type name. + errorType = exception.GetType ().FullName; + + return errorType != null; + } + + internal static TagList GetTags (Uri uri, Exception? ex) + { + var tags = new TagList { + { "url.scheme", uri.Scheme }, + { "server.address", uri.Host }, + { "server.port", uri.Port } + }; + + if (ex is not null && TryGetErrorType (ex, out var errorType)) + tags.Add ("error.type", errorType); + + return tags; + } + + public void RecordClientDisconnected (long startTimestamp, Uri uri, Exception? ex = null) + { + if (ConnectionDuration.Enabled) { + var duration = TimeSpan.FromTicks (Stopwatch.GetTimestamp () - startTimestamp).TotalSeconds; + var tags = GetTags (uri, ex); + + ConnectionDuration.Record (duration, tags); + } + } + } +} + +#endif // NET6_0_OR_GREATER diff --git a/MailKit/Net/ExtendedSslStream.cs b/MailKit/Net/ExtendedSslStream.cs new file mode 100644 index 0000000000..e208e4ea50 --- /dev/null +++ b/MailKit/Net/ExtendedSslStream.cs @@ -0,0 +1,154 @@ +// +// SslStream.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Net.Security; +using System.Diagnostics.CodeAnalysis; +using System.Security.Authentication.ExtendedProtection; + +namespace MailKit.Net +{ + class ExtendedSslStream : SslStream, IChannelBindingContext + { + ChannelBinding? tlsServerEndPoint; + ChannelBinding? tlsUnique; + + public ExtendedSslStream (Stream innerStream, bool leaveInnerStreamOpen, RemoteCertificateValidationCallback userCertificateValidationCallback) : base (innerStream, leaveInnerStreamOpen, userCertificateValidationCallback) + { + } + + new public Stream InnerStream { + get { return base.InnerStream; } + } + + ChannelBinding? GetChannelBinding (ChannelBindingKind kind) + { + ChannelBinding? channelBinding; + + try { + // Note: Documentation for TransportContext.GetChannelBinding() states that it will return null if the + // requested channel binding type is not supported, but it also states that it will throw + // NotSupportedException, so we handle both. + channelBinding = TransportContext?.GetChannelBinding (kind); + } catch (NotSupportedException) { + return null; + } + + if (channelBinding == null || channelBinding.IsClosed || channelBinding.IsInvalid) + return null; + + return channelBinding; + } + + /// + /// Try to get a channel-binding. + /// + /// + /// Tries to get the specified channel-binding. + /// + /// The kind of channel-binding desired. + /// The channel-binding. + /// if the channel-binding token was acquired; otherwise, . + public bool TryGetChannelBinding (ChannelBindingKind kind, [NotNullWhen (true)] out ChannelBinding? channelBinding) + { + int identifierLength; + + if (kind == ChannelBindingKind.Endpoint) { + channelBinding = tlsServerEndPoint ??= GetChannelBinding (kind); + identifierLength = "tls-server-end-point:".Length; + } else if (kind == ChannelBindingKind.Unique) { + channelBinding = tlsUnique ??= GetChannelBinding (kind); + identifierLength = "tls-unique:".Length; + } else { + channelBinding = null; + return false; + } + + if (channelBinding == null || channelBinding.Size <= 32 + identifierLength) + return false; + + return true; + } + + /// + /// Try to get a channel-binding token. + /// + /// + /// Tries to get the specified channel-binding. + /// + /// The kind of channel-binding desired. + /// The channel-binding token. + /// if the channel-binding token was acquired; otherwise, . + public bool TryGetChannelBindingToken (ChannelBindingKind kind, [NotNullWhen (true)] out byte[]? token) + { + token = null; + + if (!TryGetChannelBinding (kind, out var channelBinding)) + return false; + + int identifierLength; + + if (kind == ChannelBindingKind.Endpoint) { + identifierLength = "tls-server-end-point:".Length; + } else if (kind == ChannelBindingKind.Unique) { + identifierLength = "tls-unique:".Length; + } else { + return false; + } + + int tokenLength = (channelBinding.Size - 32) - identifierLength; + token = new byte[tokenLength]; + + unsafe { + byte* inbuf = (byte*) channelBinding.DangerousGetHandle ().ToPointer (); + byte* inptr = inbuf + 32 + identifierLength; + byte* inend = inbuf + channelBinding.Size; + + fixed (byte* outbuf = token) { + byte* outptr = outbuf; + + while (inptr < inend) + *outptr++ = *inptr++; + } + } + + return true; + } + + protected override void Dispose (bool disposing) + { + if (disposing) { + tlsServerEndPoint?.Close (); + tlsServerEndPoint = null; + tlsUnique?.Close (); + tlsUnique = null; + } + + base.Dispose (disposing); + } + } +} diff --git a/MailKit/Net/IChannelBindingContext.cs b/MailKit/Net/IChannelBindingContext.cs new file mode 100644 index 0000000000..a34d703604 --- /dev/null +++ b/MailKit/Net/IChannelBindingContext.cs @@ -0,0 +1,61 @@ +// +// IChannelBindingContext.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System.Diagnostics.CodeAnalysis; +using System.Security.Authentication.ExtendedProtection; + +namespace MailKit.Net { + /// + /// An interface for resources that support acquiring channel-binding tokens. + /// + /// + /// An interface for resources that support acquiring channel-binding tokens. + /// + interface IChannelBindingContext + { + /// + /// Try to get a channel-binding. + /// + /// + /// Tries to get the specified channel-binding. + /// + /// The kind of channel-binding desired. + /// The channel-binding. + /// if the channel-binding token was acquired; otherwise, . + bool TryGetChannelBinding (ChannelBindingKind kind, [NotNullWhen (true)] out ChannelBinding? channelBinding); + + /// + /// Try to get a channel-binding token. + /// + /// + /// Tries to get the specified channel-binding token. + /// + /// The kind of channel-binding desired. + /// The channel-binding token. + /// if the channel-binding token was acquired; otherwise, . + bool TryGetChannelBindingToken (ChannelBindingKind kind, [NotNullWhen (true)] out byte[]? token); + } +} diff --git a/MailKit/Net/Imap/AsyncImapClient.cs b/MailKit/Net/Imap/AsyncImapClient.cs new file mode 100644 index 0000000000..83a05f0ed0 --- /dev/null +++ b/MailKit/Net/Imap/AsyncImapClient.cs @@ -0,0 +1,1357 @@ +// +// AsyncImapClient.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Net; +using System.Text; +using System.Threading; +using System.Net.Sockets; +using System.Net.Security; +using System.Threading.Tasks; +using System.Collections.Generic; + +using MailKit.Security; + +namespace MailKit.Net.Imap +{ + public partial class ImapClient + { + /// + /// Asynchronously enable compression over the IMAP connection. + /// + /// + /// Asynchronously enables compression over the IMAP connection. + /// If the IMAP server supports the extension, + /// it is possible at any point after connecting to enable compression to reduce network + /// bandwidth usage. Ideally, this method should be called before authenticating. + /// + /// An asynchronous task context. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// Compression must be enabled before a folder has been selected. + /// + /// + /// The IMAP server does not support the COMPRESS extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server replied to the COMPRESS command with a NO or BAD response. + /// + /// + /// An IMAP protocol error occurred. + /// + public async Task CompressAsync (CancellationToken cancellationToken = default) + { + var ic = QueueCompressCommand (cancellationToken); + + await engine.RunAsync (ic).ConfigureAwait (false); + + ProcessCompressResponse (ic); + } + + /// + /// Asynchronously enable the QRESYNC feature. + /// + /// + /// Enables the QRESYNC feature. + /// The QRESYNC extension improves resynchronization performance of folders by + /// querying the IMAP server for a list of changes when the folder is opened using the + /// + /// method. + /// If this feature is enabled, the event is replaced + /// with the event. + /// This method needs to be called immediately after calling one of the + /// Authenticate methods, before + /// opening any folders. + /// + /// An asynchronous task context. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Quick resynchronization needs to be enabled before selecting a folder. + /// + /// + /// The IMAP server does not support the QRESYNC extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server replied to the ENABLE command with a NO or BAD response. + /// + /// + /// An IMAP protocol error occurred. + /// + public override async Task EnableQuickResyncAsync (CancellationToken cancellationToken = default) + { + if (!TryQueueEnableQuickResyncCommand (cancellationToken, out var ic)) + return; + + await engine.RunAsync (ic).ConfigureAwait (false); + + ProcessEnableResponse (ic); + } + + /// + /// Asynchronously enable the UTF8=ACCEPT extension. + /// + /// + /// Enables the UTF8=ACCEPT extension. + /// + /// An asynchronous task context. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// UTF8=ACCEPT needs to be enabled before selecting a folder. + /// + /// + /// The IMAP server does not support the UTF8=ACCEPT extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server replied to the ENABLE command with a NO or BAD response. + /// + /// + /// An IMAP protocol error occurred. + /// + public async Task EnableUTF8Async (CancellationToken cancellationToken = default) + { + if (!TryQueueEnableUTF8Command (cancellationToken, out var ic)) + return; + + await engine.RunAsync (ic).ConfigureAwait (false); + + ProcessEnableResponse (ic); + } + + /// + /// Asynchronously identify the client implementation to the server and obtain the server implementation details. + /// + /// + /// Passes along the client implementation details to the server while also obtaining implementation + /// details from the server. + /// If the is or no properties have been set, no + /// identifying information will be sent to the server. + /// + /// Security Implications + /// This command has the danger of violating the privacy of users if misused. Clients should + /// notify users that they send the ID command. + /// It is highly desirable that implementations provide a method of disabling ID support, perhaps by + /// not calling this method at all, or by passing as the + /// argument. + /// Implementors must exercise extreme care in adding properties to the . + /// Some properties, such as a processor ID number, Ethernet address, or other unique (or mostly unique) identifier + /// would allow tracking of users in ways that violate user privacy expectations and may also make it easier for + /// attackers to exploit security holes in the client. + /// + /// + /// The implementation details of the server if available; otherwise, . + /// The client implementation. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The IMAP server does not support the ID extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server replied to the ID command with a NO or BAD response. + /// + /// + /// An IMAP protocol error occurred. + /// + public async Task IdentifyAsync (ImapImplementation clientImplementation, CancellationToken cancellationToken = default) + { + var ic = QueueIdentifyCommand (clientImplementation, cancellationToken); + + await engine.RunAsync (ic).ConfigureAwait (false); + + return ProcessIdentifyResponse (ic); + } + + async Task OnAuthenticatedAsync (string message, CancellationToken cancellationToken) + { + await engine.QueryNamespacesAsync (cancellationToken).ConfigureAwait (false); + await engine.QuerySpecialFoldersAsync (cancellationToken).ConfigureAwait (false); + OnAuthenticated (message); + } + + /// + /// Asynchronously authenticate using the specified SASL mechanism. + /// + /// + /// Authenticates using the specified SASL mechanism. + /// For a list of available SASL authentication mechanisms supported by the server, + /// check the property after the service has been + /// connected. + /// + /// An asynchronous task context. + /// The SASL mechanism. + /// The cancellation token. + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is already authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Authentication using the supplied credentials has failed. + /// + /// + /// A SASL authentication error occurred. + /// + /// + /// An I/O error occurred. + /// + /// + /// An IMAP command failed. + /// + /// + /// An IMAP protocol error occurred. + /// + public override async Task AuthenticateAsync (SaslMechanism mechanism, CancellationToken cancellationToken = default) + { + CheckCanAuthenticate (mechanism, cancellationToken); + + int capabilitiesVersion = engine.CapabilitiesVersion; + ImapCommand? ic = null; + + ConfigureSaslMechanism (mechanism); + + var command = string.Format ("AUTHENTICATE {0}", mechanism.MechanismName); + + if ((engine.Capabilities & ImapCapabilities.SaslIR) != 0 && mechanism.SupportsInitialResponse) { + string ir = await mechanism.ChallengeAsync (null, cancellationToken).ConfigureAwait (false); + command += " " + ir + "\r\n"; + } else { + command += "\r\n"; + } + + ic = engine.QueueCommand (cancellationToken, null, command); + ic.ContinuationHandler = async (imap, cmd, text, xdoAsync) => { + string challenge = await mechanism.ChallengeAsync (text, cmd.CancellationToken).ConfigureAwait (false); + var buf = Encoding.ASCII.GetBytes (challenge + "\r\n"); + + await imap.Stream!.WriteAsync (buf, 0, buf.Length, cmd.CancellationToken).ConfigureAwait (false); + await imap.Stream.FlushAsync (cmd.CancellationToken).ConfigureAwait (false); + }; + + using var operation = engine.StartNetworkOperation (NetworkOperationKind.Authenticate); + + try { + detector.IsAuthenticating = true; + + try { + await engine.RunAsync (ic).ConfigureAwait (false); + } finally { + detector.IsAuthenticating = false; + } + + ProcessAuthenticateResponse (ic, mechanism); + + // Query the CAPABILITIES again if the server did not include an + // untagged CAPABILITIES response to the AUTHENTICATE command. + if (engine.CapabilitiesVersion == capabilitiesVersion) + await engine.QueryCapabilitiesAsync (cancellationToken).ConfigureAwait (false); + + await OnAuthenticatedAsync (ic.ResponseText ?? string.Empty, cancellationToken).ConfigureAwait (false); + } catch (Exception ex) { + operation.SetError (ex); + throw; + } + } + + /// + /// Asynchronously authenticate using the supplied credentials. + /// + /// + /// Asynchronously authenticates using the supplied credentials. + /// If the IMAP server supports one or more SASL authentication mechanisms, + /// then the SASL mechanisms that both the client and server support (not including + /// any OAUTH mechanisms) are tried in order of greatest security to weakest security. + /// Once a SASL authentication mechanism is found that both client and server support, + /// the credentials are used to authenticate. + /// If the server does not support SASL or if no common SASL mechanisms + /// can be found, then LOGIN command is used as a fallback. + /// To prevent the usage of certain authentication mechanisms, + /// simply remove them from the hash set + /// before calling this method. + /// + /// An asynchronous task context. + /// The text encoding to use for the user's credentials. + /// The user's credentials. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is already authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Authentication using the supplied credentials has failed. + /// + /// + /// A SASL authentication error occurred. + /// + /// + /// An I/O error occurred. + /// + /// + /// An IMAP command failed. + /// + /// + /// An IMAP protocol error occurred. + /// + public override async Task AuthenticateAsync (Encoding encoding, ICredentials credentials, CancellationToken cancellationToken = default) + { + CheckCanAuthenticate (encoding, credentials); + + using var operation = engine.StartNetworkOperation (NetworkOperationKind.Authenticate); + + try { + int capabilitiesVersion = engine.CapabilitiesVersion; + var uri = new Uri ("imap://" + engine.Uri!.Host); + NetworkCredential? cred; + ImapCommand? ic = null; + SaslMechanism? sasl; + string id; + + foreach (var authmech in SaslMechanism.Rank (engine.AuthenticationMechanisms)) { + cred = credentials.GetCredential (uri, authmech); + + if (cred == null || (sasl = SaslMechanism.Create (authmech, encoding, cred)) == null) + continue; + + ConfigureSaslMechanism (sasl, uri); + + cancellationToken.ThrowIfCancellationRequested (); + + var command = string.Format ("AUTHENTICATE {0}", sasl.MechanismName); + + if ((engine.Capabilities & ImapCapabilities.SaslIR) != 0 && sasl.SupportsInitialResponse) { + string ir = await sasl.ChallengeAsync (null, cancellationToken).ConfigureAwait (false); + + command += " " + ir + "\r\n"; + } else { + command += "\r\n"; + } + + ic = engine.QueueCommand (cancellationToken, null, command); + ic.ContinuationHandler = async (imap, cmd, text, xdoAsync) => { + string challenge = await sasl.ChallengeAsync (text, cmd.CancellationToken).ConfigureAwait (false); + var buf = Encoding.ASCII.GetBytes (challenge + "\r\n"); + + await imap.Stream!.WriteAsync (buf, 0, buf.Length, cmd.CancellationToken).ConfigureAwait (false); + await imap.Stream.FlushAsync (cmd.CancellationToken).ConfigureAwait (false); + }; + + detector.IsAuthenticating = true; + + try { + await engine.RunAsync (ic).ConfigureAwait (false); + } finally { + detector.IsAuthenticating = false; + } + + if (ic.Response != ImapCommandResponse.Ok) { + EmitAndThrowOnAlert (ic); + if (ic.Bye) + throw ImapProtocolException.Create (ic); + continue; + } + + engine.State = ImapEngineState.Authenticated; + + id = GetSessionIdentifier (cred.UserName); + if (id != identifier) { + engine.FolderCache.Clear (); + identifier = id; + } + + // Query the CAPABILITIES again if the server did not include an + // untagged CAPABILITIES response to the AUTHENTICATE command. + if (engine.CapabilitiesVersion == capabilitiesVersion) + await engine.QueryCapabilitiesAsync (cancellationToken).ConfigureAwait (false); + + await OnAuthenticatedAsync (ic.ResponseText ?? string.Empty, cancellationToken).ConfigureAwait (false); + return; + } + + CheckCanLogin (ic); + + // fall back to the classic LOGIN command... + if ((cred = credentials.GetCredential (uri, "DEFAULT")) == null) + throw new AuthenticationException ("No credentials could be found for the IMAP server."); + + ic = engine.QueueCommand (cancellationToken, null, "LOGIN %S %S\r\n", cred.UserName, cred.Password); + + detector.IsAuthenticating = true; + + try { + await engine.RunAsync (ic).ConfigureAwait (false); + } finally { + detector.IsAuthenticating = false; + } + + if (ic.Response != ImapCommandResponse.Ok) + throw CreateAuthenticationException (ic); + + engine.State = ImapEngineState.Authenticated; + + id = GetSessionIdentifier (cred.UserName); + if (id != identifier) { + engine.FolderCache.Clear (); + identifier = id; + } + + // Query the CAPABILITIES again if the server did not include an + // untagged CAPABILITIES response to the LOGIN command. + if (engine.CapabilitiesVersion == capabilitiesVersion) + await engine.QueryCapabilitiesAsync (cancellationToken).ConfigureAwait (false); + + await OnAuthenticatedAsync (ic.ResponseText ?? string.Empty, cancellationToken).ConfigureAwait (false); + } catch (Exception ex) { + operation.SetError (ex); + throw; + } + } + + async Task SslHandshakeAsync (SslStream ssl, string host, CancellationToken cancellationToken) + { +#if NET5_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER + await ssl.AuthenticateAsClientAsync (GetSslClientAuthenticationOptions (host, ValidateRemoteCertificate), cancellationToken).ConfigureAwait (false); +#else + await ssl.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, CheckCertificateRevocation).ConfigureAwait (false); +#endif + } + + async Task PostConnectAsync (Stream stream, string host, int port, SecureSocketOptions options, bool starttls, CancellationToken cancellationToken) + { + try { + ProtocolLogger.LogConnect (engine.Uri!); + } catch { + stream.Dispose (); + throw; + } + + connecting = true; + + var imap = new ImapStream (stream, ProtocolLogger); + + try { + await engine.ConnectAsync (imap, cancellationToken).ConfigureAwait (false); + } catch { + connecting = false; + throw; + } + + try { + // Only query the CAPABILITIES if the greeting didn't include them. + if (engine.CapabilitiesVersion == 0) + await engine.QueryCapabilitiesAsync (cancellationToken).ConfigureAwait (false); + + if (options == SecureSocketOptions.StartTls && (engine.Capabilities & ImapCapabilities.StartTLS) == 0) + throw new NotSupportedException ("The IMAP server does not support the STARTTLS extension."); + + if (starttls && (engine.Capabilities & ImapCapabilities.StartTLS) != 0) { + var ic = engine.QueueCommand (cancellationToken, null, "STARTTLS\r\n"); + + await engine.RunAsync (ic).ConfigureAwait (false); + + if (ic.Response == ImapCommandResponse.Ok) { + try { + var tls = new ExtendedSslStream (stream, false, ValidateRemoteCertificate); + imap.SetStream (tls); + + await SslHandshakeAsync (tls, host, cancellationToken).ConfigureAwait (false); + } catch (Exception ex) { + throw SslHandshakeException.Create (ref sslValidationInfo, ex, true, "IMAP", host, port, 993, 143); + } + + engine.IsSecure = true; + + // Query the CAPABILITIES again if the server did not include an + // untagged CAPABILITIES response to the STARTTLS command. + if (engine.CapabilitiesVersion == 1) + await engine.QueryCapabilitiesAsync (cancellationToken).ConfigureAwait (false); + } else if (options == SecureSocketOptions.StartTls) { + throw ImapCommandException.Create ("STARTTLS", ic); + } + } + } catch (Exception ex) { + engine.Disconnect (ex); + throw; + } finally { + connecting = false; + } + + // Note: we capture the state here in case someone calls Authenticate() from within the Connected event handler. + var authenticated = engine.State == ImapEngineState.Authenticated; + + OnConnected (host, port, options); + + if (authenticated) + await OnAuthenticatedAsync (string.Empty, cancellationToken).ConfigureAwait (false); + } + + /// + /// Asynchronously establish a connection to the specified IMAP server. + /// + /// + /// Establishes a connection to the specified IMAP or IMAP/S server. + /// If the has a value of 0, then the + /// parameter is used to determine the default port to + /// connect to. The default port used with + /// is 993. All other values will use a default port of 143. + /// If the has a value of + /// , then the is used + /// to determine the default security options. If the has a value + /// of 993, then the default options used will be + /// . All other values will use + /// . + /// Once a connection is established, properties such as + /// and will be + /// populated. + /// + /// + /// + /// + /// An asynchronous task context. + /// The host name to connect to. + /// The port to connect to. If the specified port is 0, then the default port will be used. + /// The secure socket options to when connecting. + /// The cancellation token. + /// + /// is . + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The has been disposed. + /// + /// + /// The is already connected. + /// + /// + /// was set to + /// + /// and the IMAP server does not support the STARTTLS extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// A socket error occurred trying to connect to the remote host. + /// + /// + /// An error occurred during the SSL/TLS negotiations. + /// + /// + /// An I/O error occurred. + /// + /// + /// An IMAP command failed. + /// + /// + /// An IMAP protocol error occurred. + /// + public override async Task ConnectAsync (string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default) + { + CheckCanConnect (host, port); + + ComputeDefaultValues (host, ref port, ref options, out var uri, out var starttls); + + using var operation = engine.StartNetworkOperation (NetworkOperationKind.Connect, uri); + + try { + var stream = await ConnectNetworkAsync (host, port, cancellationToken).ConfigureAwait (false); + stream.WriteTimeout = timeout; + stream.ReadTimeout = timeout; + + engine.Uri = uri; + + if (options == SecureSocketOptions.SslOnConnect) { + var ssl = new ExtendedSslStream (stream, false, ValidateRemoteCertificate); + + try { + await SslHandshakeAsync (ssl, host, cancellationToken).ConfigureAwait (false); + } catch (Exception ex) { + ssl.Dispose (); + + throw SslHandshakeException.Create (ref sslValidationInfo, ex, false, "IMAP", host, port, 993, 143); + } + + stream = ssl; + } + + await PostConnectAsync (stream, host, port, options, starttls, cancellationToken).ConfigureAwait (false); + } catch (Exception ex) { + operation.SetError (ex); + throw; + } + } + + /// + /// Asynchronously establish a connection to the specified IMAP or IMAP/S server using the provided socket. + /// + /// + /// Establishes a connection to the specified IMAP or IMAP/S server using + /// the provided socket. + /// If the has a value of + /// , then the is used + /// to determine the default security options. If the has a value + /// of 993, then the default options used will be + /// . All other values will use + /// . + /// Once a connection is established, properties such as + /// and will be + /// populated. + /// With the exception of using the to determine the + /// default to use when the value + /// is , the and + /// parameters are only used for logging purposes. + /// + /// An asynchronous task context. + /// The socket to use for the connection. + /// The host name to connect to. + /// The port to connect to. If the specified port is 0, then the default port will be used. + /// The secure socket options to when connecting. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// is not between 0 and 65535. + /// + /// + /// is not connected. + /// -or- + /// The is a zero-length string. + /// + /// + /// The has been disposed. + /// + /// + /// The is already connected. + /// + /// + /// was set to + /// + /// and the IMAP server does not support the STARTTLS extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An error occurred during the SSL/TLS negotiations. + /// + /// + /// An I/O error occurred. + /// + /// + /// An IMAP command failed. + /// + /// + /// An IMAP protocol error occurred. + /// + public override Task ConnectAsync (Socket socket, string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default) + { + CheckCanConnect (socket, host, port); + + return ConnectAsync (new NetworkStream (socket, true), host, port, options, cancellationToken); + } + + /// + /// Asynchronously establish a connection to the specified IMAP or IMAP/S server using the provided stream. + /// + /// + /// Establishes a connection to the specified IMAP or IMAP/S server using + /// the provided stream. + /// If the has a value of + /// , then the is used + /// to determine the default security options. If the has a value + /// of 993, then the default options used will be + /// . All other values will use + /// . + /// Once a connection is established, properties such as + /// and will be + /// populated. + /// With the exception of using the to determine the + /// default to use when the value + /// is , the and + /// parameters are only used for logging purposes. + /// + /// An asynchronous task context. + /// The stream to use for the connection. + /// The host name to connect to. + /// The port to connect to. If the specified port is 0, then the default port will be used. + /// The secure socket options to when connecting. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The has been disposed. + /// + /// + /// The is already connected. + /// + /// + /// was set to + /// + /// and the IMAP server does not support the STARTTLS extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An error occurred during the SSL/TLS negotiations. + /// + /// + /// An I/O error occurred. + /// + /// + /// An IMAP command failed. + /// + /// + /// An IMAP protocol error occurred. + /// + public override async Task ConnectAsync (Stream stream, string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default) + { + CheckCanConnect (stream, host, port); + + ComputeDefaultValues (host, ref port, ref options, out var uri, out var starttls); + + using var operation = engine.StartNetworkOperation (NetworkOperationKind.Connect, uri); + + try { + Stream network; + + engine.Uri = uri; + + if (options == SecureSocketOptions.SslOnConnect) { + var ssl = new ExtendedSslStream (stream, false, ValidateRemoteCertificate); + + try { + await SslHandshakeAsync (ssl, host, cancellationToken).ConfigureAwait (false); + } catch (Exception ex) { + ssl.Dispose (); + + throw SslHandshakeException.Create (ref sslValidationInfo, ex, false, "IMAP", host, port, 993, 143); + } + + network = ssl; + } else { + network = stream; + } + + if (network.CanTimeout) { + network.WriteTimeout = timeout; + network.ReadTimeout = timeout; + } + + await PostConnectAsync (network, host, port, options, starttls, cancellationToken).ConfigureAwait (false); + } catch (Exception ex) { + operation.SetError (ex); + throw; + } + } + + /// + /// Asynchronously disconnect the service. + /// + /// + /// If is , a LOGOUT command will be issued in order to disconnect cleanly. + /// + /// + /// + /// + /// An asynchronous task context. + /// If set to , a LOGOUT command will be issued in order to disconnect cleanly. + /// The cancellation token. + /// + /// The has been disposed. + /// + public override async Task DisconnectAsync (bool quit, CancellationToken cancellationToken = default) + { + CheckDisposed (); + + if (!engine.IsConnected) + return; + + if (quit) { + try { + var ic = engine.QueueCommand (cancellationToken, null, "LOGOUT\r\n"); + await engine.RunAsync (ic).ConfigureAwait (false); + } catch (OperationCanceledException) { + } catch (ImapProtocolException) { + } catch (ImapCommandException) { + } catch (IOException) { + } + } + + disconnecting = true; + + engine.Disconnect (null); + } + + /// + /// Asynchronously ping the IMAP server to keep the connection alive. + /// + /// + /// The NOOP command is typically used to keep the connection with the IMAP server + /// alive. When a client goes too long (typically 30 minutes) without sending any commands to the + /// IMAP server, the IMAP server will close the connection with the client, forcing the client to + /// reconnect before it can send any more commands. + /// The NOOP command also provides a great way for a client to check for new + /// messages. + /// When the IMAP server receives a NOOP command, it will reply to the client with a + /// list of pending updates such as EXISTS and RECENT counts on the currently + /// selected folder. To receive these notifications, subscribe to the + /// and events, + /// respectively. + /// For more information about the NOOP command, see + /// rfc3501. + /// + /// + /// + /// + /// An asynchronous task context. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server replied to the NOOP command with a NO or BAD response. + /// + /// + /// The server responded with an unexpected token. + /// + public override async Task NoOpAsync (CancellationToken cancellationToken = default) + { + var ic = QueueNoOpCommand (cancellationToken); + + await engine.RunAsync (ic).ConfigureAwait (false); + + ProcessNoOpResponse (ic); + } + + /// + /// Asynchronously toggle the into the IDLE state. + /// + /// + /// When a client enters the IDLE state, the IMAP server will send + /// events to the client as they occur on the selected folder. These events + /// may include notifications of new messages arriving, expunge notifications, + /// flag changes, etc. + /// Due to the nature of the IDLE command, a folder must be selected + /// before a client can enter into the IDLE state. This can be done by + /// opening a folder using + /// + /// or any of the other variants. + /// While the IDLE command is running, no other commands may be issued until the + /// is cancelled. + /// It is especially important to cancel the + /// before cancelling the when using SSL or TLS due to + /// the fact that cannot be polled. + /// + /// An asynchronous task context. + /// The cancellation token used to return to the non-idle state. + /// The cancellation token. + /// + /// must be cancellable (i.e. cannot be used). + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// A has not been opened. + /// + /// + /// The IMAP server does not support the IDLE extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server replied to the IDLE command with a NO or BAD response. + /// + /// + /// The server responded with an unexpected token. + /// + public async Task IdleAsync (CancellationToken doneToken, CancellationToken cancellationToken = default) + { + CheckCanIdle (doneToken); + + if (doneToken.IsCancellationRequested) + return; + + using (var context = new ImapIdleContext (engine, doneToken, cancellationToken)) { + var ic = QueueIdleCommand (context, cancellationToken); + + await engine.RunAsync (ic).ConfigureAwait (false); + + ProcessIdleResponse (ic); + } + } + + /// + /// Asynchronously request the specified notification events from the IMAP server. + /// + /// + /// The NOTIFY command is used to expand + /// which notifications the client wishes to be notified about, including status notifications + /// about folders other than the currently selected folder. It can also be used to automatically + /// FETCH information about new messages that have arrived in the currently selected folder. + /// This, combined with , + /// can be used to get instant notifications for changes to any of the specified folders. + /// + /// An asynchronous task context. + /// if the server should immediately notify the client of the + /// selected folder's status; otherwise, . + /// The specific event groups that the client would like to receive notifications for. + /// The cancellation token. + /// + /// is . + /// + /// + /// is empty. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// One or more is invalid. + /// + /// + /// The IMAP server does not support the NOTIFY extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server replied to the NOTIFY command with a NO or BAD response. + /// + /// + /// The server responded with an unexpected token. + /// + public async Task NotifyAsync (bool status, IList eventGroups, CancellationToken cancellationToken = default) + { + var ic = QueueNotifyCommand (status, eventGroups, cancellationToken, out bool notifySelectedNewExpunge); + + await engine.RunAsync (ic).ConfigureAwait (false); + + ProcessNotifyResponse (ic, notifySelectedNewExpunge); + } + + /// + /// Asynchronously disable any previously requested notification events from the IMAP server. + /// + /// + /// Disables any notification events requested in a prior call to + /// . + /// request. + /// + /// An asynchronous task context. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The IMAP server does not support the NOTIFY extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server replied to the NOTIFY command with a NO or BAD response. + /// + /// + /// The server responded with an unexpected token. + /// + public async Task DisableNotifyAsync (CancellationToken cancellationToken = default) + { + var ic = QueueDisableNotifyCommand (cancellationToken); + + await engine.RunAsync (ic).ConfigureAwait (false); + + ProcessNotifyResponse (ic, false); + } + + /// + /// Asynchronously get all of the folders within the specified namespace. + /// + /// + /// Gets all of the folders within the specified namespace. + /// + /// The folders. + /// The namespace. + /// The status items to pre-populate. + /// If set to , only subscribed folders will be listed. + /// The cancellation token. + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// The namespace folder could not be found. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server replied to the LIST or LSUB command with a NO or BAD response. + /// + /// + /// The server responded with an unexpected token. + /// + public override Task> GetFoldersAsync (FolderNamespace @namespace, StatusItems items = StatusItems.None, bool subscribedOnly = false, CancellationToken cancellationToken = default) + { + if (@namespace == null) + throw new ArgumentNullException (nameof (@namespace)); + + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); + + return engine.GetFoldersAsync (@namespace, items, subscribedOnly, cancellationToken); + } + + /// + /// Asynchronously get the folder for the specified path. + /// + /// + /// Gets the folder for the specified path. + /// + /// The folder. + /// The folder path. + /// The cancellation token. + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// The folder could not be found. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server replied to the LIST command with a NO or BAD response. + /// + /// + /// The server responded with an unexpected token. + /// + public override Task GetFolderAsync (string path, CancellationToken cancellationToken = default) + { + if (path == null) + throw new ArgumentNullException (nameof (path)); + + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); + + return engine.GetFolderAsync (path, cancellationToken); + } + + /// + /// Asynchronously gets the specified metadata. + /// + /// + /// Gets the specified metadata. + /// + /// The requested metadata value. + /// The metadata tag. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The IMAP server does not support the METADATA or METADATA-SERVER extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override async Task GetMetadataAsync (MetadataTag tag, CancellationToken cancellationToken = default) + { + var ic = QueueGetMetadataCommand (tag, cancellationToken); + + await engine.RunAsync (ic).ConfigureAwait (false); + + return ProcessGetMetadataResponse (ic, tag); + } + + /// + /// Asynchronously gets the specified metadata. + /// + /// + /// Gets the specified metadata. + /// + /// The requested metadata. + /// The metadata options. + /// The metadata tags. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The IMAP server does not support the METADATA or METADATA-SERVER extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override async Task GetMetadataAsync (MetadataOptions options, IEnumerable tags, CancellationToken cancellationToken = default) + { + if (!TryQueueGetMetadataCommand (options, tags, cancellationToken, out var ic)) + return new MetadataCollection (); + + await engine.RunAsync (ic).ConfigureAwait (false); + + return ProcessGetMetadataResponse (ic, options); + } + + /// + /// Asynchronously gets the specified metadata. + /// + /// + /// Sets the specified metadata. + /// + /// An asynchronous task context. + /// The metadata. + /// The cancellation token. + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The IMAP server does not support the METADATA or METADATA-SERVER extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override async Task SetMetadataAsync (MetadataCollection metadata, CancellationToken cancellationToken = default) + { + if (!TryQueueSetMetadataCommand (metadata, cancellationToken, out var ic)) + return; + + await engine.RunAsync (ic).ConfigureAwait (false); + + ProcessSetMetadataResponse (ic); + } + } +} diff --git a/MailKit/Net/Imap/IImapClient.cs b/MailKit/Net/Imap/IImapClient.cs new file mode 100644 index 0000000000..3d57e8e838 --- /dev/null +++ b/MailKit/Net/Imap/IImapClient.cs @@ -0,0 +1,628 @@ +// +// IImapClient.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; + +namespace MailKit.Net.Imap { + /// + /// An interface for an IMAP client. + /// + /// + /// Implemented by . + /// + public interface IImapClient : IMailStore + { + /// + /// Get the capabilities supported by the IMAP server. + /// + /// + /// The capabilities will not be known until a successful connection has been made via one of + /// the Connect methods and may + /// change as a side-effect of calling one of the + /// Authenticate + /// methods. + /// + /// + /// + /// + /// The capabilities. + /// + /// Capabilities cannot be enabled, they may only be disabled. + /// + ImapCapabilities Capabilities { get; set; } + + /// + /// Get the maximum size of a message that can be appended to a folder. + /// + /// + /// Gets the maximum size of a message, in bytes, that can be appended to a folder. + /// If the value is not set, then the limit is unspecified. + /// + /// The append limit. + uint? AppendLimit { get; } + + /// + /// Get the internationalization level supported by the IMAP server. + /// + /// + /// Gets the internationalization level supported by the IMAP server. + /// For more information, see + /// section 4 of rfc5255. + /// + /// The internationalization level. + int InternationalizationLevel { get; } + + /// + /// Get the access rights supported by the IMAP server. + /// + /// + /// These rights are additional rights supported by the IMAP server beyond the standard rights + /// defined in section 2.1 of rfc4314 + /// and will not be populated until the client is successfully connected. + /// + /// + /// + /// + /// The rights. + AccessRights Rights { get; } + + /// + /// Get whether or not the client is currently in the IDLE state. + /// + /// + /// Gets whether or not the client is currently in the IDLE state. + /// + /// if an IDLE command is active; otherwise, . + bool IsIdle { get; } + + /// + /// Enable compression over the IMAP connection. + /// + /// + /// Enables compression over the IMAP connection. + /// If the IMAP server supports the extension, + /// it is possible at any point after connecting to enable compression to reduce network + /// bandwidth usage. Ideally, this method should be called before authenticating. + /// + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// Compression must be enabled before a folder has been selected. + /// + /// + /// The IMAP server does not support the extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server replied to the COMPRESS command with a NO or BAD response. + /// + /// + /// An IMAP protocol error occurred. + /// + void Compress (CancellationToken cancellationToken = default); + + /// + /// Asynchronously enable compression over the IMAP connection. + /// + /// + /// Asynchronously enables compression over the IMAP connection. + /// If the IMAP server supports the extension, + /// it is possible at any point after connecting to enable compression to reduce network + /// bandwidth usage. Ideally, this method should be called before authenticating. + /// + /// An asynchronous task context. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// Compression must be enabled before a folder has been selected. + /// + /// + /// The IMAP server does not support the COMPRESS extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server replied to the COMPRESS command with a NO or BAD response. + /// + /// + /// An IMAP protocol error occurred. + /// + Task CompressAsync (CancellationToken cancellationToken = default); + + /// + /// Enable the UTF8=ACCEPT extension. + /// + /// + /// Enables the UTF8=ACCEPT extension. + /// + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// UTF8=ACCEPT needs to be enabled before selecting a folder. + /// + /// + /// The IMAP server does not support the UTF8=ACCEPT extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server replied to the ENABLE command with a NO or BAD response. + /// + /// + /// An IMAP protocol error occurred. + /// + void EnableUTF8 (CancellationToken cancellationToken = default); + + /// + /// Asynchronously enable the UTF8=ACCEPT extension. + /// + /// + /// Enables the UTF8=ACCEPT extension. + /// + /// An asynchronous task context. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// UTF8=ACCEPT needs to be enabled before selecting a folder. + /// + /// + /// The IMAP server does not support the UTF8=ACCEPT extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server replied to the ENABLE command with a NO or BAD response. + /// + /// + /// An IMAP protocol error occurred. + /// + Task EnableUTF8Async (CancellationToken cancellationToken = default); + + /// + /// Identify the client implementation to the server and obtain the server implementation details. + /// + /// + /// Passes along the client implementation details to the server while also obtaining implementation + /// details from the server. + /// If the is or no properties have been set, no + /// identifying information will be sent to the server. + /// + /// Security Implications + /// This command has the danger of violating the privacy of users if misused. Clients should + /// notify users that they send the ID command. + /// It is highly desirable that implementations provide a method of disabling ID support, perhaps by + /// not calling this method at all, or by passing as the + /// argument. + /// Implementors must exercise extreme care in adding properties to the . + /// Some properties, such as a processor ID number, Ethernet address, or other unique (or mostly unique) identifier + /// would allow tracking of users in ways that violate user privacy expectations and may also make it easier for + /// attackers to exploit security holes in the client. + /// + /// + /// + /// + /// + /// The implementation details of the server if available; otherwise, . + /// The client implementation. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The IMAP server does not support the ID extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server replied to the ID command with a NO or BAD response. + /// + /// + /// An IMAP protocol error occurred. + /// + ImapImplementation Identify (ImapImplementation clientImplementation, CancellationToken cancellationToken = default); + + /// + /// Asynchronously identify the client implementation to the server and obtain the server implementation details. + /// + /// + /// Passes along the client implementation details to the server while also obtaining implementation + /// details from the server. + /// If the is or no properties have been set, no + /// identifying information will be sent to the server. + /// + /// Security Implications + /// This command has the danger of violating the privacy of users if misused. Clients should + /// notify users that they send the ID command. + /// It is highly desirable that implementations provide a method of disabling ID support, perhaps by + /// not calling this method at all, or by passing as the + /// argument. + /// Implementors must exercise extreme care in adding properties to the . + /// Some properties, such as a processor ID number, Ethernet address, or other unique (or mostly unique) identifier + /// would allow tracking of users in ways that violate user privacy expectations and may also make it easier for + /// attackers to exploit security holes in the client. + /// + /// + /// The implementation details of the server if available; otherwise, . + /// The client implementation. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The IMAP server does not support the ID extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server replied to the ID command with a NO or BAD response. + /// + /// + /// An IMAP protocol error occurred. + /// + Task IdentifyAsync (ImapImplementation clientImplementation, CancellationToken cancellationToken = default); + + /// + /// Toggle the into the IDLE state. + /// + /// + /// When a client enters the IDLE state, the IMAP server will send + /// events to the client as they occur on the selected folder. These events + /// may include notifications of new messages arriving, expunge notifications, + /// flag changes, etc. + /// Due to the nature of the IDLE command, a folder must be selected + /// before a client can enter into the IDLE state. This can be done by + /// opening a folder using + /// + /// or any of the other variants. + /// While the IDLE command is running, no other commands may be issued until the + /// is cancelled. + /// It is especially important to cancel the + /// before cancelling the when using SSL or TLS due to + /// the fact that cannot be polled. + /// + /// + /// + /// + /// The cancellation token used to return to the non-idle state. + /// The cancellation token. + /// + /// must be cancellable (i.e. cannot be used). + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// A has not been opened. + /// + /// + /// The IMAP server does not support the IDLE extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server replied to the IDLE command with a NO or BAD response. + /// + /// + /// The server responded with an unexpected token. + /// + void Idle (CancellationToken doneToken, CancellationToken cancellationToken = default); + + /// + /// Asynchronously toggle the into the IDLE state. + /// + /// + /// When a client enters the IDLE state, the IMAP server will send + /// events to the client as they occur on the selected folder. These events + /// may include notifications of new messages arriving, expunge notifications, + /// flag changes, etc. + /// Due to the nature of the IDLE command, a folder must be selected + /// before a client can enter into the IDLE state. This can be done by + /// opening a folder using + /// + /// or any of the other variants. + /// While the IDLE command is running, no other commands may be issued until the + /// is cancelled. + /// It is especially important to cancel the + /// before cancelling the when using SSL or TLS due to + /// the fact that cannot be polled. + /// + /// An asynchronous task context. + /// The cancellation token used to return to the non-idle state. + /// The cancellation token. + /// + /// must be cancellable (i.e. cannot be used). + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// A has not been opened. + /// + /// + /// The IMAP server does not support the IDLE extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server replied to the IDLE command with a NO or BAD response. + /// + /// + /// The server responded with an unexpected token. + /// + Task IdleAsync (CancellationToken doneToken, CancellationToken cancellationToken = default); + + /// + /// Request the specified notification events from the IMAP server. + /// + /// + /// The NOTIFY command is used to expand + /// which notifications the client wishes to be notified about, including status notifications + /// about folders other than the currently selected folder. It can also be used to automatically + /// FETCH information about new messages that have arrived in the currently selected folder. + /// This, combined with , + /// can be used to get instant notifications for changes to any of the specified folders. + /// + /// if the server should immediately notify the client of the + /// selected folder's status; otherwise, . + /// The specific event groups that the client would like to receive notifications for. + /// The cancellation token. + /// + /// is . + /// + /// + /// is empty. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// One or more is invalid. + /// + /// + /// The IMAP server does not support the NOTIFY extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server replied to the NOTIFY command with a NO or BAD response. + /// + /// + /// The server responded with an unexpected token. + /// + void Notify (bool status, IList eventGroups, CancellationToken cancellationToken = default); + + /// + /// Asynchronously request the specified notification events from the IMAP server. + /// + /// + /// The NOTIFY command is used to expand + /// which notifications the client wishes to be notified about, including status notifications + /// about folders other than the currently selected folder. It can also be used to automatically + /// FETCH information about new messages that have arrived in the currently selected folder. + /// This, combined with , + /// can be used to get instant notifications for changes to any of the specified folders. + /// + /// An asynchronous task context. + /// if the server should immediately notify the client of the + /// selected folder's status; otherwise, . + /// The specific event groups that the client would like to receive notifications for. + /// The cancellation token. + /// + /// is . + /// + /// + /// is empty. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// One or more is invalid. + /// + /// + /// The IMAP server does not support the NOTIFY extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server replied to the NOTIFY command with a NO or BAD response. + /// + /// + /// The server responded with an unexpected token. + /// + Task NotifyAsync (bool status, IList eventGroups, CancellationToken cancellationToken = default); + + /// + /// Disable any previously requested notification events from the IMAP server. + /// + /// + /// Disables any notification events requested in a prior call to + /// . + /// request. + /// + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The IMAP server does not support the NOTIFY extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server replied to the NOTIFY command with a NO or BAD response. + /// + /// + /// The server responded with an unexpected token. + /// + void DisableNotify (CancellationToken cancellationToken = default); + + /// + /// Asynchronously disable any previously requested notification events from the IMAP server. + /// + /// + /// Disables any notification events requested in a prior call to + /// . + /// request. + /// + /// An asynchronous task context. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The IMAP server does not support the NOTIFY extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server replied to the NOTIFY command with a NO or BAD response. + /// + /// + /// The server responded with an unexpected token. + /// + Task DisableNotifyAsync (CancellationToken cancellationToken = default); + } +} diff --git a/MailKit/Net/Imap/IImapFolder.cs b/MailKit/Net/Imap/IImapFolder.cs new file mode 100644 index 0000000000..126025b01b --- /dev/null +++ b/MailKit/Net/Imap/IImapFolder.cs @@ -0,0 +1,877 @@ +// +// IImapFolder.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; + +using MimeKit; + +using MailKit.Search; + +namespace MailKit.Net.Imap { + /// + /// An interface for an IMAP folder. + /// + /// + /// Implemented by . + /// + /// + /// + /// + /// + /// + /// + public interface IImapFolder : IMailFolder + { + /// + /// Get the specified body part headers. + /// + /// + /// Gets the specified body part headers. + /// + /// The body part headers. + /// The UID of the message. + /// The body part specifier. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested body part headers. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + HeaderList GetHeaders (UniqueId uid, string partSpecifier, CancellationToken cancellationToken = default, ITransferProgress? progress = null); + + /// + /// Asynchronously get the specified body part headers. + /// + /// + /// Gets the specified body part headers. + /// + /// The body part headers. + /// The UID of the message. + /// The body part specifier. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested body part headers. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + Task GetHeadersAsync (UniqueId uid, string partSpecifier, CancellationToken cancellationToken = default, ITransferProgress? progress = null); + + /// + /// Get the specified body part headers. + /// + /// + /// Gets the specified body part headers. + /// + /// The body part headers. + /// The index of the message. + /// The body part specifier. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is out of range. + /// + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested body part headers. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + HeaderList GetHeaders (int index, string partSpecifier, CancellationToken cancellationToken = default, ITransferProgress? progress = null); + + /// + /// Asynchronously get the specified body part headers. + /// + /// + /// Gets the specified body part headers. + /// + /// The body part headers. + /// The index of the message. + /// The body part specifier. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is out of range. + /// + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested body part headers. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + Task GetHeadersAsync (int index, string partSpecifier, CancellationToken cancellationToken = default, ITransferProgress? progress = null); + + /// + /// Get the specified body part. + /// + /// + /// Gets the specified body part. + /// For more information about how to construct the , + /// see RFC3501, Section 6.4.5. + /// + /// The body part. + /// The UID of the message. + /// The body part specifier. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message body. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + MimeEntity GetBodyPart (UniqueId uid, string partSpecifier, CancellationToken cancellationToken = default, ITransferProgress? progress = null); + + /// + /// Asynchronously get the specified body part. + /// + /// + /// Gets the specified body part. + /// For more information about how to construct the , + /// see RFC3501, Section 6.4.5. + /// + /// The body part. + /// The UID of the message. + /// The body part specifier. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message body. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + Task GetBodyPartAsync (UniqueId uid, string partSpecifier, CancellationToken cancellationToken = default, ITransferProgress? progress = null); + + /// + /// Get the specified body part. + /// + /// + /// Gets the specified body part. + /// For more information about how to construct the , + /// see RFC3501, Section 6.4.5. + /// + /// The body part. + /// The index of the message. + /// The body part specifier. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + MimeEntity GetBodyPart (int index, string partSpecifier, CancellationToken cancellationToken = default, ITransferProgress? progress = null); + + /// + /// Asynchronously get the specified body part. + /// + /// + /// Gets the specified body part. + /// For more information about how to construct the , + /// see RFC3501, Section 6.4.5. + /// + /// The body part. + /// The index of the message. + /// The body part specifier. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + Task GetBodyPartAsync (int index, string partSpecifier, CancellationToken cancellationToken = default, ITransferProgress? progress = null); + + /// + /// Get the streams for the specified messages. + /// + /// + /// Gets the streams for the specified messages. + /// + /// The uids of the messages. + /// A callback method that gets called for each stream as it is received. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + void GetStreams (IList uids, ImapFetchStreamCallback callback, CancellationToken cancellationToken = default, ITransferProgress? progress = null); + + /// + /// Asynchronously get the streams for the specified messages. + /// + /// + /// Asynchronously gets the streams for the specified messages. + /// + /// An awaitable task. + /// The uids of the messages. + /// A callback method that gets called for each stream as it is received. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + Task GetStreamsAsync (IList uids, ImapFetchStreamAsyncCallback callback, CancellationToken cancellationToken = default, ITransferProgress? progress = null); + + /// + /// Get the streams for the specified messages. + /// + /// + /// Gets the streams for the specified messages. + /// + /// The indexes of the messages. + /// A callback method that gets called for each stream as it is received. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + void GetStreams (IList indexes, ImapFetchStreamCallback callback, CancellationToken cancellationToken = default, ITransferProgress? progress = null); + + /// + /// Asynchronously get the streams for the specified messages. + /// + /// + /// Asynchronously gets the streams for the specified messages. + /// + /// An awaitable task. + /// The indexes of the messages. + /// A callback method that gets called for each stream as it is received. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + Task GetStreamsAsync (IList indexes, ImapFetchStreamAsyncCallback callback, CancellationToken cancellationToken = default, ITransferProgress? progress = null); + + /// + /// Get the streams for the specified messages. + /// + /// + /// Gets the streams for the specified messages. + /// + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// A callback method that gets called for each stream as it is received. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is out of range. + /// -or- + /// is out of range. + /// + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + void GetStreams (int min, int max, ImapFetchStreamCallback callback, CancellationToken cancellationToken = default, ITransferProgress? progress = null); + + /// + /// Asynchronously get the streams for the specified messages. + /// + /// + /// Asynchronously gets the streams for the specified messages. + /// + /// An awaitable task. + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// A callback method that gets called for each stream as it is received. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is out of range. + /// -or- + /// is out of range. + /// + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + Task GetStreamsAsync (int min, int max, ImapFetchStreamAsyncCallback callback, CancellationToken cancellationToken = default, ITransferProgress? progress = null); + + /// + /// Search the folder for messages matching the specified query. + /// + /// + /// Sends a UID SEARCH command with the specified query passed directly to the IMAP server + /// with no interpretation by MailKit. This means that the query may contain any arguments that a + /// UID SEARCH command is allowed to have according to the IMAP specifications and any + /// extensions that are supported, including RETURN parameters. + /// + /// An array of matching UIDs. + /// The search query. + /// The cancellation token. + /// + /// is . + /// + /// + /// is an empty string. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + SearchResults Search (string query, CancellationToken cancellationToken = default); + + /// + /// Asynchronously search the folder for messages matching the specified query. + /// + /// + /// Sends a UID SEARCH command with the specified query passed directly to the IMAP server + /// with no interpretation by MailKit. This means that the query may contain any arguments that a + /// UID SEARCH command is allowed to have according to the IMAP specifications and any + /// extensions that are supported, including RETURN parameters. + /// + /// An array of matching UIDs. + /// The search query. + /// The cancellation token. + /// + /// is . + /// + /// + /// is an empty string. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + Task SearchAsync (string query, CancellationToken cancellationToken = default); + + /// + /// Sort messages matching the specified query. + /// + /// + /// Sends a UID SORT command with the specified query passed directly to the IMAP server + /// with no interpretation by MailKit. This means that the query may contain any arguments that a + /// UID SORT command is allowed to have according to the IMAP specifications and any + /// extensions that are supported, including RETURN parameters. + /// + /// An array of matching UIDs. + /// The search query. + /// The cancellation token. + /// + /// is . + /// + /// + /// is an empty string. + /// + /// + /// The IMAP server does not support the SORT extension. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + SearchResults Sort (string query, CancellationToken cancellationToken = default); + + /// + /// Asynchronously sort messages matching the specified query. + /// + /// + /// Sends a UID SORT command with the specified query passed directly to the IMAP server + /// with no interpretation by MailKit. This means that the query may contain any arguments that a + /// UID SORT command is allowed to have according to the IMAP specifications and any + /// extensions that are supported, including RETURN parameters. + /// + /// An array of matching UIDs. + /// The search query. + /// The cancellation token. + /// + /// is . + /// + /// + /// is an empty string. + /// + /// + /// The IMAP server does not support the SORT extension. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + Task SortAsync (string query, CancellationToken cancellationToken = default); + } +} diff --git a/MailKit/Net/Imap/ImapAuthenticationSecretDetector.cs b/MailKit/Net/Imap/ImapAuthenticationSecretDetector.cs new file mode 100644 index 0000000000..05b96b91b8 --- /dev/null +++ b/MailKit/Net/Imap/ImapAuthenticationSecretDetector.cs @@ -0,0 +1,404 @@ +// +// ImapAuthenticationSecretDetector.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Collections.Generic; + +namespace MailKit.Net.Imap { + class ImapAuthenticationSecretDetector : IAuthenticationSecretDetector + { + static readonly IList EmptyAuthSecrets = Array.Empty (); + + enum ImapAuthCommandState + { + None, + Command, + Authenticate, + AuthMechanism, + AuthNewLine, + AuthToken, + Login, + UserName, + Password, + LoginNewLine, + Error + } + + enum ImapLoginTokenType + { + None, + Atom, + QString, + Literal + } + + enum ImapLiteralState + { + None, + Octets, + Plus, + CloseBrace, + Literal, + Complete + } + + enum ImapQStringState + { + None, + Escaped, + EndQuote, + Complete + } + + ImapAuthCommandState commandState; + ImapLiteralState literalState; + ImapQStringState qstringState; + ImapLoginTokenType tokenType; + bool isAuthenticating; + int literalOctets; + int literalSeen; + int textIndex; + + public bool IsAuthenticating { + get { return isAuthenticating; } + set { + commandState = ImapAuthCommandState.None; + isAuthenticating = value; + ClearLoginTokenState (); + textIndex = 0; + } + } + + void ClearLoginTokenState () + { + literalState = ImapLiteralState.None; + qstringState = ImapQStringState.None; + tokenType = ImapLoginTokenType.None; + literalOctets = 0; + literalSeen = 0; + } + + bool SkipText (string text, byte[] buffer, ref int index, int endIndex) + { + while (index < endIndex && textIndex < text.Length) { + if (buffer[index] != (byte) text[textIndex]) { + commandState = ImapAuthCommandState.Error; + break; + } + + textIndex++; + index++; + } + + return textIndex == text.Length; + } + + IList DetectAuthSecrets (byte[] buffer, int offset, int endIndex) + { + int index = offset; + + if (commandState == ImapAuthCommandState.Authenticate) { + if (SkipText ("AUTHENTICATE ", buffer, ref index, endIndex)) + commandState = ImapAuthCommandState.AuthMechanism; + + if (index >= endIndex || commandState == ImapAuthCommandState.Error) + return EmptyAuthSecrets; + } + + if (commandState == ImapAuthCommandState.AuthMechanism) { + while (index < endIndex && buffer[index] != (byte) ' ' && buffer[index] != (byte) '\r') + index++; + + if (index < endIndex) { + if (buffer[index] == (byte) ' ') { + commandState = ImapAuthCommandState.AuthToken; + } else { + commandState = ImapAuthCommandState.AuthNewLine; + } + + index++; + } + + if (index >= endIndex) + return EmptyAuthSecrets; + } + + if (commandState == ImapAuthCommandState.AuthNewLine) { + if (buffer[index] == (byte) '\n') { + commandState = ImapAuthCommandState.AuthToken; + index++; + } else { + commandState = ImapAuthCommandState.Error; + } + + if (index >= endIndex || commandState == ImapAuthCommandState.Error) + return EmptyAuthSecrets; + } + + int startIndex = index; + while (index < endIndex && buffer[index] != (byte) '\r') + index++; + + if (index < endIndex) + commandState = ImapAuthCommandState.AuthNewLine; + + if (index == startIndex) + return EmptyAuthSecrets; + + var secret = new AuthenticationSecret (startIndex, index - startIndex); + + if (commandState == ImapAuthCommandState.AuthNewLine) { + index++; + + if (index < endIndex) { + if (buffer[index] == (byte) '\n') { + commandState = ImapAuthCommandState.AuthToken; + } else { + commandState = ImapAuthCommandState.Error; + } + } + } + + return new AuthenticationSecret[] { secret }; + } + + bool SkipLiteralToken (List secrets, byte[] buffer, ref int index, int endIndex, byte sentinel) + { + if (literalState == ImapLiteralState.Octets) { + while (index < endIndex && buffer[index] != (byte) '+' && buffer[index] != (byte) '}') { + int digit = buffer[index] - (byte) '0'; + literalOctets = literalOctets * 10 + digit; + index++; + } + + if (index < endIndex) { + if (buffer[index] == (byte) '+') { + literalState = ImapLiteralState.Plus; + textIndex = 0; + } else { + literalState = ImapLiteralState.CloseBrace; + textIndex = 1; + } + + index++; + } + + if (index >= endIndex) + return false; + } + + if (literalState < ImapLiteralState.Literal) { + if (SkipText ("}\r\n", buffer, ref index, endIndex)) + literalState = ImapLiteralState.Literal; + } + + if (index >= endIndex || commandState == ImapAuthCommandState.Error) + return false; + + if (literalState == ImapLiteralState.Literal) { + int skip = Math.Min (literalOctets - literalSeen, endIndex - index); + + secrets.Add (new AuthenticationSecret (index, skip)); + + literalSeen += skip; + index += skip; + + if (literalSeen == literalOctets) + literalState = ImapLiteralState.Complete; + } + + if (literalState == ImapLiteralState.Complete && index < endIndex && buffer[index] == sentinel) { + index++; + return true; + } + + return false; + } + + bool SkipLoginToken (List secrets, byte[] buffer, ref int index, int endIndex, byte sentinel) + { + int startIndex; + + if (tokenType == ImapLoginTokenType.None) { + switch ((char) buffer[index]) { + case '{': + literalState = ImapLiteralState.Octets; + tokenType = ImapLoginTokenType.Literal; + index++; + break; + case '"': + tokenType = ImapLoginTokenType.QString; + index++; + break; + default: + tokenType = ImapLoginTokenType.Atom; + break; + } + } + + switch (tokenType) { + case ImapLoginTokenType.Literal: + return SkipLiteralToken (secrets, buffer, ref index, endIndex, sentinel); + case ImapLoginTokenType.QString: + if (qstringState != ImapQStringState.Complete) { + startIndex = index; + + while (index < endIndex) { + if (qstringState == ImapQStringState.Escaped) { + qstringState = ImapQStringState.None; + } else if (buffer[index] == (byte) '\\') { + qstringState = ImapQStringState.Escaped; + } else if (buffer[index] == (byte) '"') { + qstringState = ImapQStringState.EndQuote; + break; + } + index++; + } + + if (index > startIndex) + secrets.Add (new AuthenticationSecret (startIndex, index - startIndex)); + + if (qstringState == ImapQStringState.EndQuote) { + qstringState = ImapQStringState.Complete; + index++; + } + } + + if (index >= endIndex) + return false; + + if (buffer[index] != sentinel) { + commandState = ImapAuthCommandState.Error; + return false; + } + + index++; + + return true; + default: + startIndex = index; + + while (index < endIndex && buffer[index] != sentinel) + index++; + + if (index > startIndex) + secrets.Add (new AuthenticationSecret (startIndex, index - startIndex)); + + if (index >= endIndex) + return false; + + index++; + + return true; + } + } + + IList DetectLoginSecrets (byte[] buffer, int offset, int endIndex) + { + var secrets = new List (); + int index = offset; + + if (commandState == ImapAuthCommandState.LoginNewLine) + return EmptyAuthSecrets; + + if (commandState == ImapAuthCommandState.Login) { + if (SkipText ("LOGIN ", buffer, ref index, endIndex)) + commandState = ImapAuthCommandState.UserName; + + if (index >= endIndex || commandState == ImapAuthCommandState.Error) + return EmptyAuthSecrets; + } + + if (commandState == ImapAuthCommandState.UserName) { + if (SkipLoginToken (secrets, buffer, ref index, endIndex, (byte) ' ')) { + commandState = ImapAuthCommandState.Password; + ClearLoginTokenState (); + } + + if (index >= endIndex || commandState == ImapAuthCommandState.Error) + return secrets; + } + + if (commandState == ImapAuthCommandState.Password) { + if (SkipLoginToken (secrets, buffer, ref index, endIndex, (byte) '\r')) { + commandState = ImapAuthCommandState.LoginNewLine; + ClearLoginTokenState (); + } + } + + return secrets; + } + + public IList DetectSecrets (byte[] buffer, int offset, int count) + { + if (!IsAuthenticating || commandState == ImapAuthCommandState.Error || count == 0) + return EmptyAuthSecrets; + + int endIndex = offset + count; + int index = offset; + + if (commandState == ImapAuthCommandState.None) { + // skip over the tag + while (index < endIndex && buffer[index] != (byte) ' ') + index++; + + if (index < endIndex) { + commandState = ImapAuthCommandState.Command; + index++; + } + + if (index >= endIndex) + return EmptyAuthSecrets; + } + + if (commandState == ImapAuthCommandState.Command) { + switch ((char) buffer[index]) { + case 'A': + commandState = ImapAuthCommandState.Authenticate; + textIndex = 1; + index++; + break; + case 'L': + commandState = ImapAuthCommandState.Login; + textIndex = 1; + index++; + break; + default: + commandState = ImapAuthCommandState.Error; + break; + } + + if (index >= endIndex || commandState == ImapAuthCommandState.Error) + return EmptyAuthSecrets; + } + + if (commandState >= ImapAuthCommandState.Authenticate && commandState <= ImapAuthCommandState.AuthToken) + return DetectAuthSecrets (buffer, index, endIndex); + + return DetectLoginSecrets (buffer, index, endIndex); + } + } +} diff --git a/MailKit/Net/Imap/ImapCallbacks.cs b/MailKit/Net/Imap/ImapCallbacks.cs new file mode 100644 index 0000000000..a73773e458 --- /dev/null +++ b/MailKit/Net/Imap/ImapCallbacks.cs @@ -0,0 +1,68 @@ +// +// ImapCallbacks.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace MailKit.Net.Imap +{ + /// + /// A callback used when fetching message streams. + /// + /// + /// This callback is meant to be used with the various + /// GetStreams + /// methods. + /// Once this callback returns, the stream argument will be disposed, so + /// it is important to consume the stream right away and not add it to a queue + /// for later processing. + /// + /// The IMAP folder that the message belongs to. + /// The index of the message in the folder. + /// The UID of the message in the folder. + /// The raw message (or part) stream. + public delegate void ImapFetchStreamCallback (ImapFolder folder, int index, UniqueId uid, Stream stream); + + /// + /// An asynchronous callback used when fetching message streams. + /// + /// + /// This callback is meant to be used with the various + /// GetStreamsAsync + /// methods. + /// Once this callback returns, the stream argument will be disposed, so + /// it is important to consume the stream right away and not add it to a queue + /// for later processing. + /// + /// An awaitable task context. + /// The IMAP folder that the message belongs to. + /// The index of the message in the folder. + /// The UID of the message in the folder. + /// The raw message (or part) stream. + /// The cancellation token. + public delegate Task ImapFetchStreamAsyncCallback (ImapFolder folder, int index, UniqueId uid, Stream stream, CancellationToken cancellationToken); +} diff --git a/MailKit/Net/Imap/ImapCapabilities.cs b/MailKit/Net/Imap/ImapCapabilities.cs index 4056d23315..aca89b3151 100644 --- a/MailKit/Net/Imap/ImapCapabilities.cs +++ b/MailKit/Net/Imap/ImapCapabilities.cs @@ -1,9 +1,9 @@ -// +// // ImapCapabilities.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -24,6 +24,9 @@ // THE SOFTWARE. // +// https://datatracker.ietf.org/doc/search/?name=IMAP&rfcs=on&activedrafts=on&by=group +// TODO: rfc9208 and rfc9394 + using System; namespace MailKit.Net.Imap { @@ -39,7 +42,7 @@ namespace MailKit.Net.Imap { /// /// [Flags] - public enum ImapCapabilities : long { + public enum ImapCapabilities : ulong { /// /// The server does not support any additional extensions. /// @@ -55,253 +58,303 @@ public enum ImapCapabilities : long { /// IMAP4rev1 = 1L << 1, + /// + /// The server implements the core IMAP4rev2 commands described in rfc9051. + /// + IMAP4rev2 = 1L << 2, + /// /// The server supports the STATUS command. /// - Status = 1L << 2, + Status = 1L << 3, /// /// The server supports the ACL extension defined in rfc2086 /// and rfc4314. /// - Acl = 1L << 3, + Acl = 1L << 4, /// /// The server supports the QUOTA extension. /// - Quota = 1L << 4, + Quota = 1L << 5, /// /// The server supports the LITERAL+ extension. /// - LiteralPlus = 1L << 5, + LiteralPlus = 1L << 6, /// /// The server supports the IDLE extension. /// - Idle = 1L << 6, + Idle = 1L << 7, /// /// The server supports the MAILBOX-REFERRALS extension. /// - MailboxReferrals = 1L << 7, + MailboxReferrals = 1L << 8, /// /// the server supports the LOGIN-REFERRALS extension. /// - LoginReferrals = 1L << 8, + LoginReferrals = 1L << 9, /// /// The server supports the NAMESPACE extension. /// - Namespace = 1L << 9, + Namespace = 1L << 10, /// /// The server supports the ID extension. /// - Id = 1L << 10, + Id = 1L << 11, /// /// The server supports the CHILDREN extension. /// - Children = 1L << 11, + Children = 1L << 12, /// /// The server supports the LOGINDISABLED extension. /// - LoginDisabled = 1L << 12, + LoginDisabled = 1L << 13, /// /// The server supports the STARTTLS extension. /// - StartTLS = 1L << 13, + StartTLS = 1L << 14, /// /// The server supports the MULTIAPPEND extension. /// - MultiAppend = 1L << 14, + MultiAppend = 1L << 15, /// /// The server supports the BINARY content extension. /// - Binary = 1L << 15, + Binary = 1L << 16, /// /// The server supports the UNSELECT extension. /// - Unselect = 1L << 16, + Unselect = 1L << 17, /// /// The server supports the UIDPLUS extension. /// - UidPlus = 1L << 17, + UidPlus = 1L << 18, /// /// The server supports the CATENATE extension. /// - Catenate = 1L << 18, + Catenate = 1L << 19, /// /// The server supports the CONDSTORE extension. /// - CondStore = 1L << 19, + CondStore = 1L << 20, /// /// The server supports the ESEARCH extension. /// - ESearch = 1L << 20, + ESearch = 1L << 21, /// /// The server supports the SASL-IR extension. /// - SaslIR = 1L << 21, + SaslIR = 1L << 22, /// /// The server supports the COMPRESS extension. /// - Compress = 1L << 22, + Compress = 1L << 23, /// /// The server supports the WITHIN extension. /// - Within = 1L << 23, + Within = 1L << 24, /// /// The server supports the ENABLE extension. /// - Enable = 1L << 24, + Enable = 1L << 25, /// /// The server supports the QRESYNC extension. /// - QuickResync = 1L << 25, + QuickResync = 1L << 26, /// /// The server supports the SEARCHRES extension. /// - SearchResults = 1L << 26, + SearchResults = 1L << 27, /// /// The server supports the SORT extension. /// - Sort = 1L << 27, + Sort = 1L << 28, /// /// The server supports the THREAD extension. /// - Thread = 1L << 28, + Thread = 1L << 29, + + /// + /// The server supports the ANNOTATE extension. + /// + Annotate = 1L << 30, /// /// The server supports the LIST-EXTENDED extension. /// - ListExtended = 1L << 29, + ListExtended = 1L << 31, /// /// The server supports the CONVERT extension. /// - Convert = 1L << 30, + Convert = 1L << 32, /// /// The server supports the LANGUAGE extension. /// - Language = 1L << 31, + Language = 1L << 33, /// /// The server supports the I18NLEVEL extension. /// - I18NLevel = 1L << 32, + I18NLevel = 1L << 34, /// /// The server supports the ESORT extension. /// - ESort = 1L << 33, + ESort = 1L << 35, /// /// The server supports the CONTEXT extension. /// - Context = 1L << 34, + Context = 1L << 36, /// /// The server supports the METADATA extension. /// - Metadata = 1L << 35, + Metadata = 1L << 37, + + /// + /// The server supports the METADATA-SERVER extension. + /// + MetadataServer = 1L << 38, /// /// The server supports the NOTIFY extension. /// - Notify = 1L << 36, + Notify = 1L << 39, /// /// The server supports the FILTERS extension. /// - Filters = 1L << 37, + Filters = 1L << 40, /// /// The server supports the LIST-STATUS extension. /// - ListStatus = 1L << 38, + ListStatus = 1L << 41, /// /// The server supports the SORT=DISPLAY extension. /// - SortDisplay = 1L << 39, + SortDisplay = 1L << 42, /// /// The server supports the CREATE-SPECIAL-USE extension. /// - CreateSpecialUse = 1L << 40, + CreateSpecialUse = 1L << 43, /// /// The server supports the SPECIAL-USE extension. /// - SpecialUse = 1L << 41, + SpecialUse = 1L << 44, /// /// The server supports the SEARCH=FUZZY extension. /// - FuzzySearch = 1L << 42, + FuzzySearch = 1L << 45, /// /// The server supports the MULTISEARCH extension. /// - MultiSearch = 1L << 43, + MultiSearch = 1L << 46, /// /// The server supports the MOVE extension. /// - Move = 1L << 44, + Move = 1L << 47, /// /// The server supports the UTF8=ACCEPT extension. /// - UTF8Accept = 1L << 45, + UTF8Accept = 1L << 48, /// /// The server supports the UTF8=ONLY extension. /// - UTF8Only = 1L << 46, + UTF8Only = 1L << 49, /// /// The server supports the LITERAL- extension. /// - LiteralMinus = 1L << 47, + LiteralMinus = 1L << 50, /// /// The server supports the APPENDLIMIT extension. /// - AppendLimit = 1L << 48, + AppendLimit = 1L << 51, + + /// + /// The server supports the UNAUTHENTICATE extension. + /// + Unauthenticate = 1L << 52, + + /// + /// The server supports the STATUS=SIZE extension. + /// + StatusSize = 1L << 53, + + /// + /// The server supports the LIST-MYRIGHTS extension. + /// + ListMyRights = 1L << 54, + + /// + /// The server supports the OBJECTID extension. + /// + ObjectID = 1L << 55, + + /// + /// The server supports the REPLACE extension. + /// + Replace = 1L << 56, + + /// + /// The server supports the SAVEDATE extension. + /// + SaveDate = 1L << 57, + + /// + /// The server supports the PREVIEW extension. + /// + Preview = 1L << 58, #region GMail Extensions /// /// The server supports the XLIST extension (GMail). /// - XList = 1L << 50, + XList = 1L << 60, /// /// The server supports the X-GM-EXT1 extension (GMail). /// - GMailExt1 = 1L << 51 + GMailExt1 = 1L << 61 #endregion } diff --git a/MailKit/Net/Imap/ImapClient.cs b/MailKit/Net/Imap/ImapClient.cs index 351739791a..d6ae0d6828 100644 --- a/MailKit/Net/Imap/ImapClient.cs +++ b/MailKit/Net/Imap/ImapClient.cs @@ -1,9 +1,9 @@ -// +// // ImapClient.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -29,22 +29,19 @@ using System.Net; using System.Text; using System.Threading; -using System.Threading.Tasks; -using System.Collections.Generic; - -#if NETFX_CORE -using Windows.Networking; -using Windows.Networking.Sockets; -using Windows.Storage.Streams; -using Encoding = Portable.Text.Encoding; -#else using System.Net.Sockets; using System.Net.Security; +using System.Globalization; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Security.Authentication; +using System.Diagnostics.CodeAnalysis; using System.Security.Cryptography.X509Certificates; -#endif using MailKit.Security; +using AuthenticationException = MailKit.Security.AuthenticationException; + namespace MailKit.Net.Imap { /// /// An IMAP client that can be used to retrieve messages from a server. @@ -58,23 +55,24 @@ namespace MailKit.Net.Imap { /// SSL-wrapped connection. /// /// - /// + /// /// /// - /// + /// /// - public class ImapClient : MailStore + public partial class ImapClient : MailStore, IImapClient { - static readonly char[] ReservedUriCharacters = new [] { ';', '/', '?', ':', '@', '&', '=', '+', '$', ',' }; + static readonly char[] ReservedUriCharacters = { ';', '/', '?', ':', '@', '&', '=', '+', '$', ',', '%' }; const string HexAlphabet = "0123456789ABCDEF"; + + readonly ImapAuthenticationSecretDetector detector = new ImapAuthenticationSecretDetector (); readonly ImapEngine engine; -#if NETFX_CORE - StreamSocket socket; -#endif - string identifier = null; - int timeout = 100000; + SslCertificateValidationInfo? sslValidationInfo; + int timeout = 2 * 60 * 1000; + string? identifier; + bool disconnecting; + bool connecting; bool disposed; - bool secure; /// /// Initializes a new instance of the class. @@ -87,7 +85,7 @@ public class ImapClient : MailStore /// methods. /// /// - /// + /// /// public ImapClient () : this (new NullProtocolLogger ()) { @@ -108,15 +106,26 @@ public ImapClient () : this (new NullProtocolLogger ()) /// /// The protocol logger. /// - /// is null. + /// is . /// public ImapClient (IProtocolLogger protocolLogger) : base (protocolLogger) { + protocolLogger.AuthenticationSecretDetector = detector; + // FIXME: should this take a ParserOptions argument? engine = new ImapEngine (CreateImapFolder); + engine.MetadataChanged += OnEngineMetadataChanged; + engine.FolderCreated += OnEngineFolderCreated; + engine.Disconnected += OnEngineDisconnected; + engine.WebAlert += OnEngineWebAlert; engine.Alert += OnEngineAlert; } + // Note: This is only needed for UnitTests. + internal char TagPrefix { + set { engine.TagPrefix = value; } + } + /// /// Gets an object that can be used to synchronize access to the IMAP server. /// @@ -169,7 +178,7 @@ public ImapCapabilities Capabilities { } /// - /// Gets the maximum size of a message that can be appended to a folder. + /// Get the maximum size of a message that can be appended to a folder. /// /// /// Gets the maximum size of a message, in bytes, that can be appended to a folder. @@ -181,7 +190,7 @@ public uint? AppendLimit { } /// - /// Gets the internationalization level supported by the IMAP server. + /// Get the internationalization level supported by the IMAP server. /// /// /// Gets the internationalization level supported by the IMAP server. @@ -235,9 +244,9 @@ void CheckAuthenticated () /// This method's purpose is to allow subclassing . /// /// The IMAP folder instance. - /// The constructior arguments. + /// The constructor arguments. /// - /// is null. + /// is . /// protected virtual ImapFolder CreateImapFolder (ImapFolderConstructorArgs args) { @@ -248,53 +257,33 @@ protected virtual ImapFolder CreateImapFolder (ImapFolderConstructorArgs args) return folder; } -#if !NETFX_CORE - bool ValidateRemoteCertificate (object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) + bool ValidateRemoteCertificate (object? sender, X509Certificate? certificate, X509Chain? chain, SslPolicyErrors sslPolicyErrors) { - if (ServerCertificateValidationCallback != null) - return ServerCertificateValidationCallback (engine.Uri.Host, certificate, chain, sslPolicyErrors); + var host = engine.Uri!.Host; + bool valid; + + sslValidationInfo?.Dispose (); + sslValidationInfo = null; -#if !NETSTANDARD - if (ServicePointManager.ServerCertificateValidationCallback != null) - return ServicePointManager.ServerCertificateValidationCallback (engine.Uri.Host, certificate, chain, sslPolicyErrors); + if (ServerCertificateValidationCallback != null) { + valid = ServerCertificateValidationCallback (host, certificate, chain, sslPolicyErrors); +#if NETFRAMEWORK + } else if (ServicePointManager.ServerCertificateValidationCallback != null) { + valid = ServicePointManager.ServerCertificateValidationCallback (host, certificate, chain, sslPolicyErrors); #endif + } else { + valid = DefaultServerCertificateValidationCallback (host, certificate, chain, sslPolicyErrors); + } + + if (!valid) { + // Note: The SslHandshakeException.Create() method will nullify this once it's done using it. + sslValidationInfo = new SslCertificateValidationInfo (host, certificate, chain, sslPolicyErrors); + } - return DefaultServerCertificateValidationCallback (sender, certificate, chain, sslPolicyErrors); + return valid; } -#endif - /// - /// Enable compression over the IMAP connection. - /// - /// - /// Enables compression over the IMAP connection. - /// If the IMAP server supports the extension, - /// it is possible at any point after connecting to enable compression to reduce network - /// bandwidth usage. Ideally, this method should be called before authenticating. - /// - /// The cancellation token. - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// Compression must be enabled before a folder has been selected. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server replied to the COMPRESS command with a NO or BAD response. - /// - /// - /// An IMAP protocol error occurred. - /// - public void Compress (CancellationToken cancellationToken = default (CancellationToken)) + ImapCommand QueueCompressCommand (CancellationToken cancellationToken) { CheckDisposed (); CheckConnected (); @@ -305,13 +294,16 @@ bool ValidateRemoteCertificate (object sender, X509Certificate certificate, X509 if (engine.State >= ImapEngineState.Selected) throw new InvalidOperationException ("Compression must be enabled before selecting a folder."); - int capabilitiesVersion = engine.CapabilitiesVersion; - var ic = engine.QueueCommand (cancellationToken, null, "COMPRESS DEFLATE\r\n"); - - engine.Wait (ic); - - ProcessResponseCodes (ic); +#if MAILKIT_LITE + throw new NotSupportedException ("MailKitLite does not support the COMPRESS extension."); +#else + return engine.QueueCommand (cancellationToken, null, "COMPRESS DEFLATE\r\n"); +#endif + } + void ProcessCompressResponse (ImapCommand ic) + { +#if !MAILKIT_LITE if (ic.Response != ImapCommandResponse.Ok) { for (int i = 0; i < ic.RespCodes.Count; i++) { if (ic.RespCodes[i].Type == ImapResponseCodeType.CompressionActive) @@ -321,24 +313,19 @@ bool ValidateRemoteCertificate (object sender, X509Certificate certificate, X509 throw ImapCommandException.Create ("COMPRESS", ic); } - engine.Stream.Stream = new CompressedStream (engine.Stream.Stream); - - // Query the CAPABILITIES again if the server did not include an - // untagged CAPABILITIES response to the COMPRESS command. - if (engine.CapabilitiesVersion == capabilitiesVersion) - engine.QueryCapabilities (cancellationToken); + engine.Stream!.SetStream (new CompressedStream (engine.Stream.Stream)); +#endif } /// - /// Asynchronously enable compression over the IMAP connection. + /// Enable compression over the IMAP connection. /// /// - /// Asynchronously enables compression over the IMAP connection. + /// Enables compression over the IMAP connection. /// If the IMAP server supports the extension, /// it is possible at any point after connecting to enable compression to reduce network /// bandwidth usage. Ideally, this method should be called before authenticating. /// - /// An asynchronous task context. /// The cancellation token. /// /// The has been disposed. @@ -350,7 +337,7 @@ bool ValidateRemoteCertificate (object sender, X509Certificate certificate, X509 /// Compression must be enabled before a folder has been selected. /// /// - /// The IMAP server does not support the COMPRESS extension. + /// The IMAP server does not support the extension. /// /// /// The operation was canceled via the cancellation token. @@ -364,20 +351,56 @@ bool ValidateRemoteCertificate (object sender, X509Certificate certificate, X509 /// /// An IMAP protocol error occurred. /// - public Task CompressAsync (CancellationToken cancellationToken = default (CancellationToken)) + public void Compress (CancellationToken cancellationToken = default) { - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - Compress (cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); + var ic = QueueCompressCommand (cancellationToken); + + engine.Run (ic); + + ProcessCompressResponse (ic); + } + + bool TryQueueEnableQuickResyncCommand (CancellationToken cancellationToken, [NotNullWhen (true)] out ImapCommand? ic) + { + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); + + if (engine.State != ImapEngineState.Authenticated) + throw new InvalidOperationException ("QRESYNC needs to be enabled immediately after authenticating."); + + if ((engine.Capabilities & ImapCapabilities.QuickResync) == 0) + throw new NotSupportedException ("The IMAP server does not support the QRESYNC extension."); + + if (engine.QResyncEnabled) { + ic = null; + return false; + } + + ic = engine.QueueCommand (cancellationToken, null, "ENABLE QRESYNC CONDSTORE\r\n"); + + return true; + } + + void ProcessEnableResponse (ImapCommand ic) + { + ic.ThrowIfNotOk ("ENABLE"); + + if (engine.QuirksMode == ImapQuirksMode.iCloud) { + // Note: iCloud's response to the `ENABLE QRESYNC CONDSTORE` command does not include an untagged response + // notifying us that QRESYNC or CONDSTORE have been enabled. Instead, if we get a tagged OK response, we + // assume that these features were enabled successfully. + // + // See https://github.com/jstedfast/MailKit/issues/1871 for details. + engine.QResyncEnabled = true; + } } /// /// Enable the QRESYNC feature. /// /// - /// Enables the QRESYNC feature. + /// Enables the QRESYNC feature. /// The QRESYNC extension improves resynchronization performance of folders by /// querying the IMAP server for a list of changes when the folder is opened using the /// @@ -416,68 +439,17 @@ bool ValidateRemoteCertificate (object sender, X509Certificate certificate, X509 /// /// An IMAP protocol error occurred. /// - public override void EnableQuickResync (CancellationToken cancellationToken = default (CancellationToken)) + public override void EnableQuickResync (CancellationToken cancellationToken = default) { - CheckDisposed (); - CheckConnected (); - CheckAuthenticated (); - - if (engine.State != ImapEngineState.Authenticated) - throw new InvalidOperationException ("QRESYNC needs to be enabled immediately after authenticating."); - - if ((engine.Capabilities & ImapCapabilities.QuickResync) == 0) - throw new NotSupportedException ("The IMAP server does not support the QRESYNC extension."); - - if (engine.QResyncEnabled) + if (!TryQueueEnableQuickResyncCommand (cancellationToken, out var ic)) return; - var ic = engine.QueueCommand (cancellationToken, null, "ENABLE QRESYNC CONDSTORE\r\n"); + engine.Run (ic); - engine.Wait (ic); - - ProcessResponseCodes (ic); - - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("ENABLE", ic); - - engine.QResyncEnabled = true; + ProcessEnableResponse (ic); } - /// - /// Enable the UTF8=ACCEPT extension. - /// - /// - /// Enables the UTF8=ACCEPT extension. - /// - /// The cancellation token. - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// UTF8=ACCEPT needs to be enabled before selecting a folder. - /// - /// - /// The IMAP server does not support the UTF8=ACCEPT extension. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server replied to the ENABLE command with a NO or BAD response. - /// - /// - /// An IMAP protocol error occurred. - /// - public void EnableUTF8 (CancellationToken cancellationToken = default (CancellationToken)) + bool TryQueueEnableUTF8Command (CancellationToken cancellationToken, [NotNullWhen (true)] out ImapCommand? ic) { CheckDisposed (); CheckConnected (); @@ -489,28 +461,22 @@ bool ValidateRemoteCertificate (object sender, X509Certificate certificate, X509 if ((engine.Capabilities & ImapCapabilities.UTF8Accept) == 0) throw new NotSupportedException ("The IMAP server does not support the UTF8=ACCEPT extension."); - if (engine.UTF8Enabled) - return; - - var ic = engine.QueueCommand (cancellationToken, null, "ENABLE UTF8=ACCEPT\r\n"); - - engine.Wait (ic); - - ProcessResponseCodes (ic); + if (engine.UTF8Enabled) { + ic = null; + return false; + } - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("ENABLE", ic); + ic = engine.QueueCommand (cancellationToken, null, "ENABLE UTF8=ACCEPT\r\n"); - engine.UTF8Enabled = true; + return true; } /// /// Enable the UTF8=ACCEPT extension. /// /// - /// Enables the UTF8=ACCEPT extension. + /// Enables the UTF8=ACCEPT extension. /// - /// An asynchronous task context. /// The cancellation token. /// /// The has been disposed. @@ -539,64 +505,17 @@ bool ValidateRemoteCertificate (object sender, X509Certificate certificate, X509 /// /// An IMAP protocol error occurred. /// - public Task EnableUTF8Async (CancellationToken cancellationToken = default (CancellationToken)) + public void EnableUTF8 (CancellationToken cancellationToken = default) { - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - EnableUTF8 (cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); + if (!TryQueueEnableUTF8Command (cancellationToken, out var ic)) + return; + + engine.Run (ic); + + ProcessEnableResponse (ic); } - /// - /// Identify the client implementation to the server and obtain the server implementation details. - /// - /// - /// Passes along the client implementation details to the server while also obtaining implementation - /// details from the server. - /// If the is null or no properties have been set, no - /// identifying information will be sent to the server. - /// - /// Security Implications - /// This command has the danger of violating the privacy of users if misused. Clients should - /// notify users that they send the ID command. - /// It is highly desirable that implementations provide a method of disabling ID support, perhaps by - /// not calling this method at all, or by passing null as the - /// argument. - /// Implementors must exercise extreme care in adding properties to the . - /// Some properties, such as a processor ID number, Ethernet address, or other unique (or mostly unique) identifier - /// would allow tracking of users in ways that violate user privacy expectations and may also make it easier for - /// attackers to exploit security holes in the client. - /// - /// - /// - /// - /// - /// The implementation details of the server if available; otherwise, null. - /// The client implementation. - /// The cancellation token. - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The IMAP server does not support the ID extension. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server replied to the ID command with a NO or BAD response. - /// - /// - /// An IMAP protocol error occurred. - /// - public ImapImplementation Identify (ImapImplementation clientImplementation, CancellationToken cancellationToken = default (CancellationToken)) + ImapCommand QueueIdentifyCommand (ImapImplementation clientImplementation, CancellationToken cancellationToken) { CheckDisposed (); CheckConnected (); @@ -627,33 +546,34 @@ bool ValidateRemoteCertificate (object sender, X509Certificate certificate, X509 } var ic = new ImapCommand (engine, cancellationToken, null, command.ToString (), args.ToArray ()); - ic.RegisterUntaggedHandler ("ID", ImapUtils.ParseImplementation); + ic.RegisterUntaggedHandler ("ID", ImapUtils.UntaggedIdHandler); engine.QueueCommand (ic); - engine.Wait (ic); - ProcessResponseCodes (ic); + return ic; + } - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("ID", ic); + static ImapImplementation ProcessIdentifyResponse (ImapCommand ic) + { + ic.ThrowIfNotOk ("ID"); - return (ImapImplementation) ic.UserData; + return (ImapImplementation) ic.UserData!; } /// - /// Asynchronously identify the client implementation to the server and obtain the server implementation details. + /// Identify the client implementation to the server and obtain the server implementation details. /// /// /// Passes along the client implementation details to the server while also obtaining implementation /// details from the server. - /// If the is null or no properties have been set, no + /// If the is or no properties have been set, no /// identifying information will be sent to the server. /// /// Security Implications /// This command has the danger of violating the privacy of users if misused. Clients should /// notify users that they send the ID command. /// It is highly desirable that implementations provide a method of disabling ID support, perhaps by - /// not calling this method at all, or by passing null as the + /// not calling this method at all, or by passing as the /// argument. /// Implementors must exercise extreme care in adding properties to the . /// Some properties, such as a processor ID number, Ethernet address, or other unique (or mostly unique) identifier @@ -661,7 +581,10 @@ bool ValidateRemoteCertificate (object sender, X509Certificate certificate, X509 /// attackers to exploit security holes in the client. /// /// - /// The implementation details of the server if available; otherwise, null. + /// + /// + /// + /// The implementation details of the server if available; otherwise, . /// The client implementation. /// The cancellation token. /// @@ -685,13 +608,13 @@ bool ValidateRemoteCertificate (object sender, X509Certificate certificate, X509 /// /// An IMAP protocol error occurred. /// - public Task IdentifyAsync (ImapImplementation clientImplementation, CancellationToken cancellationToken = default (CancellationToken)) + public ImapImplementation Identify (ImapImplementation clientImplementation, CancellationToken cancellationToken = default) { - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return Identify (clientImplementation, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); + var ic = QueueIdentifyCommand (clientImplementation, cancellationToken); + + engine.Run (ic); + + return ProcessIdentifyResponse (ic); } #region IMailService implementation @@ -726,8 +649,8 @@ public override HashSet AuthenticationMechanisms { /// /// /// - /// The authentication mechanisms. - public HashSet ThreadingAlgorithms { + /// The supported threading algorithms. + public override HashSet ThreadingAlgorithms { get { return engine.ThreadingAlgorithms; } } @@ -742,7 +665,7 @@ public HashSet ThreadingAlgorithms { public override int Timeout { get { return timeout; } set { - if (IsConnected && engine.Stream.CanTimeout) { + if (engine.IsConnected && engine.Stream.CanTimeout) { engine.Stream.WriteTimeout = value; engine.Stream.ReadTimeout = value; } @@ -755,16 +678,16 @@ public override int Timeout { /// Get whether or not the client is currently connected to an IMAP server. /// /// - /// The state is set to true immediately after + /// The state is set to immediately after /// one of the Connect - /// methods succeeds and is not set back to false until either the client + /// methods succeeds and is not set back to until either the client /// is disconnected via or until an /// is thrown while attempting to read or write to /// the underlying network socket. /// When an is caught, the connection state of the /// should be checked before continuing. /// - /// true if the client is connected; otherwise, false. + /// if the client is connected; otherwise, . public override bool IsConnected { get { return engine.IsConnected; } } @@ -775,103 +698,292 @@ public override bool IsConnected { /// /// Gets whether or not the connection is secure (typically via SSL or TLS). /// - /// true if the connection is secure; otherwise, false. + /// if the connection is secure; otherwise, . public override bool IsSecure { - get { return IsConnected && secure; } + get { return engine.IsSecure; } } /// - /// Get whether or not the client is currently authenticated with the IMAP server. + /// Get whether or not the connection is encrypted (typically via SSL or TLS). /// /// - /// Gets whether or not the client is currently authenticated with the IMAP server. - /// To authenticate with the IMAP server, use one of the - /// Authenticate - /// methods. + /// Gets whether or not the connection is encrypted (typically via SSL or TLS). /// - /// true if the client is connected; otherwise, false. - public override bool IsAuthenticated { - get { return engine.State >= ImapEngineState.Authenticated; } + /// if the connection is encrypted; otherwise, . + public override bool IsEncrypted { + get { return engine.IsSecure && (engine.Stream.Stream is SslStream sslStream) && sslStream.IsEncrypted; } } /// - /// Get whether or not the client is currently in the IDLE state. + /// Get whether or not the connection is signed (typically via SSL or TLS). /// /// - /// Gets whether or not the client is currently in the IDLE state. + /// Gets whether or not the connection is signed (typically via SSL or TLS). /// - /// true if an IDLE command is active; otherwise, false. - public bool IsIdle { - get { return engine.State == ImapEngineState.Idle; } + /// if the connection is signed; otherwise, . + public override bool IsSigned { + get { return engine.IsSecure && (engine.Stream.Stream is SslStream sslStream) && sslStream.IsSigned; } } - void ProcessResponseCodes (ImapCommand ic) - { - for (int i = 0; i < ic.RespCodes.Count; i++) { - if (ic.RespCodes[i].Type == ImapResponseCodeType.Alert) { - OnAlert (ic.RespCodes[i].Message); - break; - } + /// + /// Get the negotiated SSL or TLS protocol version. + /// + /// + /// Gets the negotiated SSL or TLS protocol version once an SSL or TLS connection has been made. + /// + /// + /// + /// + /// The negotiated SSL or TLS protocol version. + public override SslProtocols SslProtocol { + get { + if (engine.IsSecure && (engine.Stream.Stream is SslStream sslStream)) + return sslStream.SslProtocol; + + return SslProtocols.None; } } - static AuthenticationException CreateAuthenticationException (ImapCommand ic) - { - if (string.IsNullOrEmpty (ic.ResponseText)) { - for (int i = 0; i < ic.RespCodes.Count; i++) { - if (ic.RespCodes[i].IsError || ic.RespCodes[i].Type == ImapResponseCodeType.Alert) - return new AuthenticationException (ic.RespCodes[i].Message); - } + /// + /// Get the negotiated SSL or TLS cipher algorithm. + /// + /// + /// Gets the negotiated SSL or TLS cipher algorithm once an SSL or TLS connection has been made. + /// + /// + /// + /// + /// The negotiated SSL or TLS cipher algorithm. +#if NET10_0_OR_GREATER + [Obsolete ("Use SslCipherSuite instead.")] +#endif + public override CipherAlgorithmType? SslCipherAlgorithm { + get { + if (engine.IsSecure && (engine.Stream.Stream is SslStream sslStream)) + return sslStream.CipherAlgorithm; - return new AuthenticationException (); + return null; } - - return new AuthenticationException (ic.ResponseText); } - static bool IsHexDigit (char c) - { - return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'); - } + /// + /// Get the negotiated SSL or TLS cipher algorithm strength. + /// + /// + /// Gets the negotiated SSL or TLS cipher algorithm strength once an SSL or TLS connection has been made. + /// + /// + /// + /// + /// The negotiated SSL or TLS cipher algorithm strength. +#if NET10_0_OR_GREATER + [Obsolete ("Use SslCipherSuite instead.")] +#endif + public override int? SslCipherStrength { + get { + if (engine.IsSecure && (engine.Stream.Stream is SslStream sslStream)) + return sslStream.CipherStrength; - static char HexUnescape (string pattern, ref int index) - { - uint value, c; + return null; + } + } - if (pattern[index++] != '%' || !IsHexDigit (pattern[index]) || !IsHexDigit (pattern[index + 1])) - return '%'; +#if NET5_0_OR_GREATER + /// + /// Get the negotiated SSL or TLS cipher suite. + /// + /// + /// Gets the negotiated SSL or TLS cipher suite once an SSL or TLS connection has been made. + /// + /// The negotiated SSL or TLS cipher suite. + public override TlsCipherSuite? SslCipherSuite { + get { + if (engine.IsSecure && (engine.Stream.Stream is SslStream sslStream)) + return sslStream.NegotiatedCipherSuite; - c = (uint) pattern[index++]; + return null; + } + } +#endif - if (c >= 'a') - value = (((c - 'a') + 10) << 4); - else if (c >= 'A') - value = (((c - 'A') + 10) << 4); - else - value = ((c - '0') << 4); + /// + /// Get the negotiated SSL or TLS hash algorithm. + /// + /// + /// Gets the negotiated SSL or TLS hash algorithm once an SSL or TLS connection has been made. + /// + /// + /// + /// + /// The negotiated SSL or TLS hash algorithm. +#if NET10_0_OR_GREATER + [Obsolete ("Use SslCipherSuite instead.")] +#endif + public override HashAlgorithmType? SslHashAlgorithm { + get { + if (engine.IsSecure && (engine.Stream.Stream is SslStream sslStream)) + return sslStream.HashAlgorithm; - c = pattern[index++]; + return null; + } + } + + /// + /// Get the negotiated SSL or TLS hash algorithm strength. + /// + /// + /// Gets the negotiated SSL or TLS hash algorithm strength once an SSL or TLS connection has been made. + /// + /// + /// + /// + /// The negotiated SSL or TLS hash algorithm strength. +#if NET10_0_OR_GREATER + [Obsolete ("Use SslCipherSuite instead.")] +#endif + public override int? SslHashStrength { + get { + if (engine.IsSecure && (engine.Stream.Stream is SslStream sslStream)) + return sslStream.HashStrength; + + return null; + } + } + + /// + /// Get the negotiated SSL or TLS key exchange algorithm. + /// + /// + /// Gets the negotiated SSL or TLS key exchange algorithm once an SSL or TLS connection has been made. + /// + /// + /// + /// + /// The negotiated SSL or TLS key exchange algorithm. +#if NET10_0_OR_GREATER + [Obsolete ("Use SslCipherSuite instead.")] +#endif + public override ExchangeAlgorithmType? SslKeyExchangeAlgorithm { + get { + if (engine.IsSecure && (engine.Stream.Stream is SslStream sslStream)) + return sslStream.KeyExchangeAlgorithm; + + return null; + } + } + + /// + /// Get the negotiated SSL or TLS key exchange algorithm strength. + /// + /// + /// Gets the negotiated SSL or TLS key exchange algorithm strength once an SSL or TLS connection has been made. + /// + /// + /// + /// + /// The negotiated SSL or TLS key exchange algorithm strength. +#if NET10_0_OR_GREATER + [Obsolete ("Use SslCipherSuite instead.")] +#endif + public override int? SslKeyExchangeStrength { + get { + if (engine.IsSecure && (engine.Stream.Stream is SslStream sslStream)) + return sslStream.KeyExchangeStrength; + return null; + } + } + + /// + /// Get whether or not the client is currently authenticated with the IMAP server. + /// + /// + /// Gets whether or not the client is currently authenticated with the IMAP server. + /// To authenticate with the IMAP server, use one of the + /// Authenticate + /// methods. + /// + /// if the client is connected; otherwise, . + public override bool IsAuthenticated { + get { return engine.State >= ImapEngineState.Authenticated; } + } + + /// + /// Get whether or not the client is currently in the IDLE state. + /// + /// + /// Gets whether or not the client is currently in the IDLE state. + /// + /// if an IDLE command is active; otherwise, . + public bool IsIdle { + get { return engine.State == ImapEngineState.Idle; } + } + + static AuthenticationException CreateAuthenticationException (ImapCommand ic) + { + for (int i = 0; i < ic.RespCodes.Count; i++) { + if (ic.RespCodes[i].IsError || ic.RespCodes[i].Type == ImapResponseCodeType.Alert) + return new AuthenticationException (ic.RespCodes[i].Message); + } + + if (ic.ResponseText != null) + return new AuthenticationException (ic.ResponseText); + + return new AuthenticationException (); + } + + void EmitAndThrowOnAlert (ImapCommand ic) + { + for (int i = 0; i < ic.RespCodes.Count; i++) { + if (ic.RespCodes[i].Type != ImapResponseCodeType.Alert) + continue; + + OnAlert (ic.RespCodes[i].Message); + + throw new AuthenticationException (ic.ResponseText ?? ic.RespCodes[i].Message); + } + } + + static bool IsHexDigit (char c) + { + return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'); + } + + static uint HexUnescape (uint c) + { if (c >= 'a') - value |= ((c - 'a') + 10); - else if (c >= 'A') - value |= ((c - 'A') + 10); - else - value |= (c - '0'); + return (c - 'a') + 10; + + if (c >= 'A') + return (c - 'A') + 10; + + return c - '0'; + } + + static char HexUnescape (string pattern, ref int index) + { + uint value, c; + + if (pattern[index++] != '%' || !IsHexDigit (pattern[index]) || !IsHexDigit (pattern[index + 1])) + return '%'; + + c = (uint) pattern[index++]; + value = HexUnescape (c) << 4; + c = pattern[index++]; + value |= HexUnescape (c); return (char) value; } - static string UnescapeUserName (string escaped) + internal static string UnescapeUserName (string escaped) { - StringBuilder userName; - int startIndex, index; + int index; if ((index = escaped.IndexOf ('%')) == -1) return escaped; - userName = new StringBuilder (); - startIndex = 0; + var userName = new StringBuilder (escaped.Length); + int startIndex = 0; do { userName.Append (escaped, startIndex, index - startIndex); @@ -890,68 +1002,112 @@ static string UnescapeUserName (string escaped) return userName.ToString (); } - static string HexEscape (char c) + static void HexEscape (StringBuilder builder, char c) { - return "%" + HexAlphabet[(c >> 4) & 0xF] + HexAlphabet[c & 0xF]; + builder.Append ('%'); + builder.Append (HexAlphabet[(c >> 4) & 0xF]); + builder.Append (HexAlphabet[c & 0xF]); } - static string EscapeUserName (string userName) + internal static void EscapeUserName (StringBuilder builder, string userName) { - StringBuilder escaped; - int startIndex, index; - - if ((index = userName.IndexOfAny (ReservedUriCharacters)) == -1) - return userName; - - escaped = new StringBuilder (); - startIndex = 0; + int index = userName.IndexOfAny (ReservedUriCharacters); + int startIndex = 0; - do { - escaped.Append (userName, startIndex, index - startIndex); - escaped.Append (HexEscape (userName[index++])); + while (index != -1) { + builder.Append (userName, startIndex, index - startIndex); + HexEscape (builder, userName[index++]); startIndex = index; if (startIndex >= userName.Length) break; index = userName.IndexOfAny (ReservedUriCharacters, startIndex); - } while (index != -1); - - if (index == -1) - escaped.Append (userName, startIndex, userName.Length - startIndex); + } - return escaped.ToString (); + builder.Append (userName, startIndex, userName.Length - startIndex); } string GetSessionIdentifier (string userName) { - var uri = engine.Uri; + var builder = new StringBuilder (); + var uri = engine.Uri!; + + builder.Append (uri.Scheme); + builder.Append ("://"); + EscapeUserName (builder, userName); + builder.Append ('@'); + builder.Append (uri.Host); + builder.Append (':'); + builder.Append (uri.Port.ToString (CultureInfo.InvariantCulture)); + + return builder.ToString (); + } - return string.Format ("{0}://{1}@{2}:{3}", uri.Scheme, EscapeUserName (userName), uri.Host, uri.Port); + void OnAuthenticated (string message, CancellationToken cancellationToken) + { + engine.QueryNamespaces (cancellationToken); + engine.QuerySpecialFolders (cancellationToken); + OnAuthenticated (message); + } + + void CheckCanAuthenticate (SaslMechanism mechanism, CancellationToken cancellationToken) + { + if (mechanism == null) + throw new ArgumentNullException (nameof (mechanism)); + + CheckDisposed (); + CheckConnected (); + + if (engine.State >= ImapEngineState.Authenticated) + throw new InvalidOperationException ("The ImapClient is already authenticated."); + + cancellationToken.ThrowIfCancellationRequested (); + } + + void ConfigureSaslMechanism (SaslMechanism mechanism, Uri uri) + { + mechanism.ChannelBindingContext = engine.Stream!.Stream as IChannelBindingContext; + mechanism.Uri = uri; + } + + void ConfigureSaslMechanism (SaslMechanism mechanism) + { + var uri = new Uri ("imap://" + engine.Uri!.Host); + + ConfigureSaslMechanism (mechanism, uri); + } + + void ProcessAuthenticateResponse (ImapCommand ic, SaslMechanism mechanism) + { + if (ic.Response != ImapCommandResponse.Ok) { + EmitAndThrowOnAlert (ic); + + throw new AuthenticationException (); + } + + engine.State = ImapEngineState.Authenticated; + + var id = GetSessionIdentifier (mechanism.Credentials.UserName); + if (id != identifier) { + engine.FolderCache.Clear (); + identifier = id; + } } /// - /// Authenticate using the supplied credentials. + /// Authenticate using the specified SASL mechanism. /// /// - /// If the IMAP server supports one or more SASL authentication mechanisms, - /// then the SASL mechanisms that both the client and server support are tried - /// in order of greatest security to weakest security. Once a SASL - /// authentication mechanism is found that both client and server support, - /// the credentials are used to authenticate. - /// If the server does not support SASL or if no common SASL mechanisms - /// can be found, then LOGIN command is used as a fallback. - /// To prevent the usage of certain authentication mechanisms, - /// simply remove them from the hash set - /// before calling this method. + /// Authenticates using the specified SASL mechanism. + /// For a list of available SASL authentication mechanisms supported by the server, + /// check the property after the service has been + /// connected. /// - /// The text encoding to use for the user's credentials. - /// The user's credentials. + /// The SASL mechanism. /// The cancellation token. /// - /// is null. - /// -or- - /// is null. + /// is . /// /// /// The has been disposed. @@ -974,10 +1130,68 @@ string GetSessionIdentifier (string userName) /// /// An I/O error occurred. /// + /// + /// An IMAP command failed. + /// /// /// An IMAP protocol error occurred. /// - public override void Authenticate (Encoding encoding, ICredentials credentials, CancellationToken cancellationToken = default (CancellationToken)) + public override void Authenticate (SaslMechanism mechanism, CancellationToken cancellationToken = default) + { + CheckCanAuthenticate (mechanism, cancellationToken); + + int capabilitiesVersion = engine.CapabilitiesVersion; + ImapCommand? ic = null; + + ConfigureSaslMechanism (mechanism); + + var command = string.Format ("AUTHENTICATE {0}", mechanism.MechanismName); + + if ((engine.Capabilities & ImapCapabilities.SaslIR) != 0 && mechanism.SupportsInitialResponse) { + string ir = mechanism.Challenge (null, cancellationToken); + + command += " " + ir + "\r\n"; + } else { + command += "\r\n"; + } + + ic = engine.QueueCommand (cancellationToken, null, command); + ic.ContinuationHandler = (imap, cmd, text, xdoAsync) => { + string challenge = mechanism.Challenge (text, cmd.CancellationToken); + var buf = Encoding.ASCII.GetBytes (challenge + "\r\n"); + + imap.Stream!.Write (buf, 0, buf.Length, cmd.CancellationToken); + imap.Stream.Flush (cmd.CancellationToken); + + return Task.CompletedTask; + }; + + using var operation = engine.StartNetworkOperation (NetworkOperationKind.Authenticate); + + try { + detector.IsAuthenticating = true; + + try { + engine.Run (ic); + } finally { + detector.IsAuthenticating = false; + } + + ProcessAuthenticateResponse (ic, mechanism); + + // Query the CAPABILITIES again if the server did not include an + // untagged CAPABILITIES response to the AUTHENTICATE command. + if (engine.CapabilitiesVersion == capabilitiesVersion) + engine.QueryCapabilities (cancellationToken); + + OnAuthenticated (ic.ResponseText ?? string.Empty, cancellationToken); + } catch (Exception ex) { + operation.SetError (ex); + throw; + } + } + + void CheckCanAuthenticate (Encoding encoding, ICredentials credentials) { if (encoding == null) throw new ArgumentNullException (nameof (encoding)); @@ -990,144 +1204,187 @@ string GetSessionIdentifier (string userName) if (engine.State >= ImapEngineState.Authenticated) throw new InvalidOperationException ("The ImapClient is already authenticated."); + } - int capabilitiesVersion = engine.CapabilitiesVersion; - var uri = new Uri ("imap://" + engine.Uri.Host); - NetworkCredential cred; - ImapCommand ic = null; - SaslMechanism sasl; - string id; - - foreach (var authmech in SaslMechanism.AuthMechanismRank) { - if (!engine.AuthenticationMechanisms.Contains (authmech)) - continue; + void CheckCanLogin (ImapCommand? ic) + { + if ((Capabilities & ImapCapabilities.LoginDisabled) != 0) { + if (ic == null) + throw new AuthenticationException ("The LOGIN command is disabled."); - if ((sasl = SaslMechanism.Create (authmech, uri, encoding, credentials)) == null) - continue; + throw CreateAuthenticationException (ic); + } + } - cancellationToken.ThrowIfCancellationRequested (); + /// + /// Authenticate using the supplied credentials. + /// + /// + /// Authenticates using the supplied credentials. + /// If the IMAP server supports one or more SASL authentication mechanisms, + /// then the SASL mechanisms that both the client and server support (not including + /// any OAUTH mechanisms) are tried in order of greatest security to weakest security. + /// Once a SASL authentication mechanism is found that both client and server support, + /// the credentials are used to authenticate. + /// If the server does not support SASL or if no common SASL mechanisms + /// can be found, then LOGIN command is used as a fallback. + /// To prevent the usage of certain authentication mechanisms, + /// simply remove them from the hash set + /// before calling this method. + /// + /// The text encoding to use for the user's credentials. + /// The user's credentials. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is already authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Authentication using the supplied credentials has failed. + /// + /// + /// A SASL authentication error occurred. + /// + /// + /// An I/O error occurred. + /// + /// + /// An IMAP command failed. + /// + /// + /// An IMAP protocol error occurred. + /// + public override void Authenticate (Encoding encoding, ICredentials credentials, CancellationToken cancellationToken = default) + { + CheckCanAuthenticate (encoding, credentials); - var command = string.Format ("AUTHENTICATE {0}", sasl.MechanismName); + using var operation = engine.StartNetworkOperation (NetworkOperationKind.Authenticate); - if ((engine.Capabilities & ImapCapabilities.SaslIR) != 0 && sasl.SupportsInitialResponse) { - var ir = sasl.Challenge (null); - command += " " + ir + "\r\n"; - } else { - command += "\r\n"; - } + try { + int capabilitiesVersion = engine.CapabilitiesVersion; + var uri = new Uri ("imap://" + engine.Uri!.Host); + NetworkCredential? cred; + ImapCommand? ic = null; + SaslMechanism? sasl; + string id; - ic = engine.QueueCommand (cancellationToken, null, command); - ic.ContinuationHandler = (imap, cmd, text) => { - string challenge; + foreach (var authmech in SaslMechanism.Rank (engine.AuthenticationMechanisms)) { + cred = credentials.GetCredential (uri, authmech); - if (sasl.IsAuthenticated) { - // The server claims we aren't done authenticating, but our SASL mechanism thinks we are... - // Send an empty string to abort the AUTHENTICATE command. - challenge = string.Empty; - } else { - challenge = sasl.Challenge (text); - } + if (cred == null || (sasl = SaslMechanism.Create (authmech, encoding, cred)) == null) + continue; - var buf = Encoding.ASCII.GetBytes (challenge + "\r\n"); - imap.Stream.Write (buf, 0, buf.Length, cmd.CancellationToken); - imap.Stream.Flush (cmd.CancellationToken); - }; + ConfigureSaslMechanism (sasl, uri); - engine.Wait (ic); + cancellationToken.ThrowIfCancellationRequested (); - if (ic.Response != ImapCommandResponse.Ok) { - for (int i = 0; i < ic.RespCodes.Count; i++) { - if (ic.RespCodes[i].Type != ImapResponseCodeType.Alert) - continue; + var command = string.Format ("AUTHENTICATE {0}", sasl.MechanismName); - OnAlert (ic.RespCodes[i].Message); + if ((engine.Capabilities & ImapCapabilities.SaslIR) != 0 && sasl.SupportsInitialResponse) { + string ir = sasl.Challenge (null, cancellationToken); - throw new AuthenticationException (ic.ResponseText ?? ic.RespCodes[i].Message); + command += " " + ir + "\r\n"; + } else { + command += "\r\n"; } - continue; - } + ic = engine.QueueCommand (cancellationToken, null, command); + ic.ContinuationHandler = (imap, cmd, text, xdoAsync) => { + string challenge = sasl.Challenge (text, cmd.CancellationToken); - engine.State = ImapEngineState.Authenticated; + var buf = Encoding.ASCII.GetBytes (challenge + "\r\n"); - cred = credentials.GetCredential (uri, sasl.MechanismName); - id = GetSessionIdentifier (cred.UserName); - if (id != identifier) { - engine.FolderCache.Clear (); - identifier = id; - } - - // Query the CAPABILITIES again if the server did not include an - // untagged CAPABILITIES response to the AUTHENTICATE command. - if (engine.CapabilitiesVersion == capabilitiesVersion) - engine.QueryCapabilities (cancellationToken); + imap.Stream!.Write (buf, 0, buf.Length, cmd.CancellationToken); + imap.Stream.Flush (cmd.CancellationToken); - engine.QueryNamespaces (cancellationToken); - engine.QuerySpecialFolders (cancellationToken); - OnAuthenticated (ic.ResponseText); - return; - } + return Task.CompletedTask; + }; - if ((Capabilities & ImapCapabilities.LoginDisabled) != 0) { - if (ic == null) - throw new AuthenticationException ("The LOGIN command is disabled."); + detector.IsAuthenticating = true; - throw CreateAuthenticationException (ic); - } + try { + engine.Run (ic); + } finally { + detector.IsAuthenticating = false; + } - // fall back to the classic LOGIN command... - cred = credentials.GetCredential (uri, "DEFAULT"); + if (ic.Response != ImapCommandResponse.Ok) { + EmitAndThrowOnAlert (ic); + if (ic.Bye) + throw ImapProtocolException.Create (ic); + continue; + } - ic = engine.QueueCommand (cancellationToken, null, "LOGIN %S %S\r\n", cred.UserName, cred.Password); + engine.State = ImapEngineState.Authenticated; - engine.Wait (ic); + id = GetSessionIdentifier (cred.UserName); + if (id != identifier) { + engine.FolderCache.Clear (); + identifier = id; + } - ProcessResponseCodes (ic); + // Query the CAPABILITIES again if the server did not include an + // untagged CAPABILITIES response to the AUTHENTICATE command. + if (engine.CapabilitiesVersion == capabilitiesVersion) + engine.QueryCapabilities (cancellationToken); - if (ic.Response != ImapCommandResponse.Ok) - throw CreateAuthenticationException (ic); + OnAuthenticated (ic.ResponseText ?? string.Empty, cancellationToken); + return; + } - engine.State = ImapEngineState.Authenticated; + CheckCanLogin (ic); - id = GetSessionIdentifier (cred.UserName); - if (id != identifier) { - engine.FolderCache.Clear (); - identifier = id; - } + // fall back to the classic LOGIN command... + if ((cred = credentials.GetCredential (uri, "DEFAULT")) == null) + throw new AuthenticationException ("No credentials could be found for the IMAP server."); - // Query the CAPABILITIES again if the server did not include an - // untagged CAPABILITIES response to the LOGIN command. - if (engine.CapabilitiesVersion == capabilitiesVersion) - engine.QueryCapabilities (cancellationToken); + ic = engine.QueueCommand (cancellationToken, null, "LOGIN %S %S\r\n", cred.UserName, cred.Password); - engine.QueryNamespaces (cancellationToken); - engine.QuerySpecialFolders (cancellationToken); - OnAuthenticated (ic.ResponseText); - } + detector.IsAuthenticating = true; - internal void ReplayConnect (string host, Stream replayStream, CancellationToken cancellationToken = default (CancellationToken)) - { - CheckDisposed (); + try { + engine.Run (ic); + } finally { + detector.IsAuthenticating = false; + } - if (host == null) - throw new ArgumentNullException (nameof (host)); + if (ic.Response != ImapCommandResponse.Ok) + throw CreateAuthenticationException (ic); - if (replayStream == null) - throw new ArgumentNullException (nameof (replayStream)); + engine.State = ImapEngineState.Authenticated; - engine.Uri = new Uri ("imap://" + host); - engine.Connect (new ImapStream (replayStream, null, ProtocolLogger), cancellationToken); - engine.TagPrefix = 'A'; - secure = false; + id = GetSessionIdentifier (cred.UserName); + if (id != identifier) { + engine.FolderCache.Clear (); + identifier = id; + } - if (engine.CapabilitiesVersion == 0) - engine.QueryCapabilities (cancellationToken); + // Query the CAPABILITIES again if the server did not include an + // untagged CAPABILITIES response to the LOGIN command. + if (engine.CapabilitiesVersion == capabilitiesVersion) + engine.QueryCapabilities (cancellationToken); - engine.Disconnected += OnEngineDisconnected; - OnConnected (); + OnAuthenticated (ic.ResponseText ?? string.Empty, cancellationToken); + } catch (Exception ex) { + operation.SetError (ex); + throw; + } } - static void ComputeDefaultValues (string host, ref int port, ref SecureSocketOptions options, out Uri uri, out bool starttls) + internal static void ComputeDefaultValues (string host, ref int port, ref SecureSocketOptions options, out Uri uri, out bool starttls) { switch (options) { default: @@ -1147,26 +1404,124 @@ static void ComputeDefaultValues (string host, ref int port, ref SecureSocketOpt break; } + if (IPAddress.TryParse (host, out var ip) && ip.AddressFamily == AddressFamily.InterNetworkV6) + host = "[" + host + "]"; + switch (options) { case SecureSocketOptions.StartTlsWhenAvailable: - uri = new Uri ("imap://" + host + ":" + port + "/?starttls=when-available"); + uri = new Uri (string.Format (CultureInfo.InvariantCulture, "imap://{0}:{1}/?starttls=when-available", host, port)); starttls = true; break; case SecureSocketOptions.StartTls: - uri = new Uri ("imap://" + host + ":" + port + "/?starttls=always"); + uri = new Uri (string.Format (CultureInfo.InvariantCulture, "imap://{0}:{1}/?starttls=always", host, port)); starttls = true; break; case SecureSocketOptions.SslOnConnect: - uri = new Uri ("imaps://" + host + ":" + port); + uri = new Uri (string.Format (CultureInfo.InvariantCulture, "imaps://{0}:{1}", host, port)); starttls = false; break; default: - uri = new Uri ("imap://" + host + ":" + port); + uri = new Uri (string.Format (CultureInfo.InvariantCulture, "imap://{0}:{1}", host, port)); starttls = false; break; } } + void CheckCanConnect (string host, int port) + { + if (host == null) + throw new ArgumentNullException (nameof (host)); + + if (host.Length == 0) + throw new ArgumentException ("The host name cannot be empty.", nameof (host)); + + if (port < 0 || port > 65535) + throw new ArgumentOutOfRangeException (nameof (port)); + + CheckDisposed (); + + if (IsConnected) + throw new InvalidOperationException ("The ImapClient is already connected."); + } + + void SslHandshake (SslStream ssl, string host, CancellationToken cancellationToken) + { +#if NET5_0_OR_GREATER + ssl.AuthenticateAsClient (GetSslClientAuthenticationOptions (host, ValidateRemoteCertificate)); +#else + ssl.AuthenticateAsClient (host, ClientCertificates, SslProtocols, CheckCertificateRevocation); +#endif + } + + void PostConnect (Stream stream, string host, int port, SecureSocketOptions options, bool starttls, CancellationToken cancellationToken) + { + try { + ProtocolLogger.LogConnect (engine.Uri!); + } catch { + stream.Dispose (); + throw; + } + + connecting = true; + + var imap = new ImapStream (stream, ProtocolLogger); + + try { + engine.Connect (imap, cancellationToken); + } catch { + connecting = false; + throw; + } + + try { + // Only query the CAPABILITIES if the greeting didn't include them. + if (engine.CapabilitiesVersion == 0) + engine.QueryCapabilities (cancellationToken); + + if (options == SecureSocketOptions.StartTls && (engine.Capabilities & ImapCapabilities.StartTLS) == 0) + throw new NotSupportedException ("The IMAP server does not support the STARTTLS extension."); + + if (starttls && (engine.Capabilities & ImapCapabilities.StartTLS) != 0) { + var ic = engine.QueueCommand (cancellationToken, null, "STARTTLS\r\n"); + + engine.Run (ic); + + if (ic.Response == ImapCommandResponse.Ok) { + try { + var tls = new ExtendedSslStream (stream, false, ValidateRemoteCertificate); + imap.SetStream (tls); + + SslHandshake (tls, host, cancellationToken); + } catch (Exception ex) { + throw SslHandshakeException.Create (ref sslValidationInfo, ex, true, "IMAP", host, port, 993, 143); + } + + engine.IsSecure = true; + + // Query the CAPABILITIES again if the server did not include an + // untagged CAPABILITIES response to the STARTTLS command. + if (engine.CapabilitiesVersion == 1) + engine.QueryCapabilities (cancellationToken); + } else if (options == SecureSocketOptions.StartTls) { + throw ImapCommandException.Create ("STARTTLS", ic); + } + } + } catch (Exception ex) { + engine.Disconnect (ex); + throw; + } finally { + connecting = false; + } + + // Note: we capture the state here in case someone calls Authenticate() from within the Connected event handler. + var authenticated = engine.State == ImapEngineState.Authenticated; + + OnConnected (host, port, options); + + if (authenticated) + OnAuthenticated (string.Empty, cancellationToken); + } + /// /// Establish a connection to the specified IMAP server. /// @@ -1187,14 +1542,14 @@ static void ComputeDefaultValues (string host, ref int port, ref SecureSocketOpt /// populated. /// /// - /// + /// /// /// The host name to connect to. /// The port to connect to. If the specified port is 0, then the default port will be used. /// The secure socket options to when connecting. /// The cancellation token. /// - /// is null. + /// is . /// /// /// is not between 0 and 65535. @@ -1222,178 +1577,141 @@ static void ComputeDefaultValues (string host, ref int port, ref SecureSocketOpt /// /// An I/O error occurred. /// + /// + /// An IMAP command failed. + /// /// /// An IMAP protocol error occurred. /// - public override void Connect (string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default (CancellationToken)) + public override void Connect (string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default) { - if (host == null) - throw new ArgumentNullException (nameof (host)); + CheckCanConnect (host, port); - if (host.Length == 0) - throw new ArgumentException ("The host name cannot be empty.", nameof (host)); + ComputeDefaultValues (host, ref port, ref options, out var uri, out var starttls); - if (port < 0 || port > 65535) - throw new ArgumentOutOfRangeException (nameof (port)); + using var operation = engine.StartNetworkOperation (NetworkOperationKind.Connect, uri); - CheckDisposed (); - - if (IsConnected) - throw new InvalidOperationException ("The ImapClient is already connected."); - - Stream stream; - bool starttls; - Uri uri; - - ComputeDefaultValues (host, ref port, ref options, out uri, out starttls); - -#if !NETFX_CORE -#if NETSTANDARD - var ipAddresses = Dns.GetHostAddressesAsync (uri.DnsSafeHost).GetAwaiter ().GetResult (); -#else - var ipAddresses = Dns.GetHostAddresses (uri.DnsSafeHost); -#endif - Socket socket = null; - - for (int i = 0; i < ipAddresses.Length; i++) { - socket = new Socket (ipAddresses[i].AddressFamily, SocketType.Stream, ProtocolType.Tcp); + try { + var stream = ConnectNetwork (host, port, cancellationToken); + stream.WriteTimeout = timeout; + stream.ReadTimeout = timeout; - try { - cancellationToken.ThrowIfCancellationRequested (); + engine.Uri = uri; - if (LocalEndPoint != null) - socket.Bind (LocalEndPoint); + if (options == SecureSocketOptions.SslOnConnect) { + var ssl = new ExtendedSslStream (stream, false, ValidateRemoteCertificate); - socket.Connect (ipAddresses[i], port); - break; - } catch (OperationCanceledException) { - socket.Dispose (); - throw; - } catch { - socket.Dispose (); + try { + SslHandshake (ssl, host, cancellationToken); + } catch (Exception ex) { + ssl.Dispose (); - if (i + 1 == ipAddresses.Length) - throw; - } - } - - if (socket == null) - throw new IOException (string.Format ("Failed to resolve host: {0}", host)); - - engine.Uri = uri; - - if (options == SecureSocketOptions.SslOnConnect) { - var ssl = new SslStream (new NetworkStream (socket, true), false, ValidateRemoteCertificate); + throw SslHandshakeException.Create (ref sslValidationInfo, ex, false, "IMAP", host, port, 993, 143); + } - try { -#if NETSTANDARD - ssl.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, true).GetAwaiter ().GetResult (); -#else - ssl.AuthenticateAsClient (host, ClientCertificates, SslProtocols, true); -#endif - } catch { - ssl.Dispose (); - throw; + stream = ssl; } - secure = true; - stream = ssl; - } else { - stream = new NetworkStream (socket, true); - secure = false; - } -#else - var protection = options == SecureSocketOptions.SslOnConnect ? SocketProtectionLevel.Tls12 : SocketProtectionLevel.PlainSocket; - socket = new StreamSocket (); - - try { - cancellationToken.ThrowIfCancellationRequested (); - socket.ConnectAsync (new HostName (host), port.ToString (), protection) - .AsTask (cancellationToken) - .GetAwaiter () - .GetResult (); - } catch { - socket.Dispose (); - socket = null; + PostConnect (stream, host, port, options, starttls, cancellationToken); + } catch (Exception ex) { + operation.SetError (ex); throw; } + } - stream = new DuplexStream (socket.InputStream.AsStreamForRead (0), socket.OutputStream.AsStreamForWrite (0)); - secure = options == SecureSocketOptions.SslOnConnect; - engine.Uri = uri; -#endif - - if (stream.CanTimeout) { - stream.WriteTimeout = timeout; - stream.ReadTimeout = timeout; - } - - ProtocolLogger.LogConnect (uri); - - engine.Connect (new ImapStream (stream, socket, ProtocolLogger), cancellationToken); - - try { - // Only query the CAPABILITIES if the greeting didn't include them. - if (engine.CapabilitiesVersion == 0) - engine.QueryCapabilities (cancellationToken); - - if (options == SecureSocketOptions.StartTls && (engine.Capabilities & ImapCapabilities.StartTLS) == 0) - throw new NotSupportedException ("The IMAP server does not support the STARTTLS extension."); - - if (starttls && (engine.Capabilities & ImapCapabilities.StartTLS) != 0) { - var ic = engine.QueueCommand (cancellationToken, null, "STARTTLS\r\n"); + void CheckCanConnect (Stream stream, string host, int port) + { + if (stream == null) + throw new ArgumentNullException (nameof (stream)); - engine.Wait (ic); + CheckCanConnect (host, port); + } - ProcessResponseCodes (ic); + void CheckCanConnect (Socket socket, string host, int port) + { + if (socket == null) + throw new ArgumentNullException (nameof (socket)); - if (ic.Response == ImapCommandResponse.Ok) { -#if !NETFX_CORE - var tls = new SslStream (stream, false, ValidateRemoteCertificate); -#if NETSTANDARD - tls.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, true).GetAwaiter ().GetResult (); -#else - tls.AuthenticateAsClient (host, ClientCertificates, SslProtocols, true); -#endif - engine.Stream.Stream = tls; -#else - socket.UpgradeToSslAsync (SocketProtectionLevel.Tls12, new HostName (host)) - .AsTask (cancellationToken) - .GetAwaiter () - .GetResult (); -#endif + if (!socket.Connected) + throw new ArgumentException ("The socket is not connected.", nameof (socket)); - secure = true; + CheckCanConnect (host, port); + } - // Query the CAPABILITIES again if the server did not include an - // untagged CAPABILITIES response to the STARTTLS command. - if (engine.CapabilitiesVersion == 1) - engine.QueryCapabilities (cancellationToken); - } else if (options == SecureSocketOptions.StartTls) { - throw ImapCommandException.Create ("STARTTLS", ic); - } - } - } catch { - engine.Disconnect (); - secure = false; - throw; - } + /// + /// Establish a connection to the specified IMAP or IMAP/S server using the provided socket. + /// + /// + /// Establishes a connection to the specified IMAP or IMAP/S server using + /// the provided socket. + /// If the has a value of + /// , then the is used + /// to determine the default security options. If the has a value + /// of 993, then the default options used will be + /// . All other values will use + /// . + /// Once a connection is established, properties such as + /// and will be + /// populated. + /// With the exception of using the to determine the + /// default to use when the value + /// is , the and + /// parameters are only used for logging purposes. + /// + /// The socket to use for the connection. + /// The host name to connect to. + /// The port to connect to. If the specified port is 0, then the default port will be used. + /// The secure socket options to when connecting. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// is not between 0 and 65535. + /// + /// + /// is not connected. + /// -or- + /// The is a zero-length string. + /// + /// + /// The has been disposed. + /// + /// + /// The is already connected. + /// + /// + /// was set to + /// + /// and the IMAP server does not support the STARTTLS extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// An IMAP command failed. + /// + /// + /// An IMAP protocol error occurred. + /// + public override void Connect (Socket socket, string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default) + { + CheckCanConnect (socket, host, port); - engine.Disconnected += OnEngineDisconnected; - OnConnected (); + Connect (new NetworkStream (socket, true), host, port, options, cancellationToken); } -#if !NETFX_CORE /// - /// Establish a connection to the specified IMAP or IMAP/S server using the provided socket. + /// Establish a connection to the specified IMAP or IMAP/S server using the provided stream. /// /// /// Establishes a connection to the specified IMAP or IMAP/S server using - /// the provided socket. - /// If the has a value of 0, then the - /// parameter is used to determine the default port to - /// connect to. The default port used with - /// is 993. All other values will use a default port of 143. + /// the provided stream. /// If the has a value of /// , then the is used /// to determine the default security options. If the has a value @@ -1403,23 +1721,25 @@ static void ComputeDefaultValues (string host, ref int port, ref SecureSocketOpt /// Once a connection is established, properties such as /// and will be /// populated. + /// With the exception of using the to determine the + /// default to use when the value + /// is , the and + /// parameters are only used for logging purposes. /// - /// The socket to use for the connection. + /// The stream to use for the connection. /// The host name to connect to. /// The port to connect to. If the specified port is 0, then the default port will be used. /// The secure socket options to when connecting. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// is not between 0 and 65535. /// /// - /// is not connected. - /// -or- /// The is a zero-length string. /// /// @@ -1439,129 +1759,68 @@ static void ComputeDefaultValues (string host, ref int port, ref SecureSocketOpt /// /// An I/O error occurred. /// + /// + /// An IMAP command failed. + /// /// /// An IMAP protocol error occurred. /// - public void Connect (Socket socket, string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default (CancellationToken)) + public override void Connect (Stream stream, string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default) { - if (socket == null) - throw new ArgumentNullException (nameof (socket)); - - if (!socket.Connected) - throw new ArgumentException ("The socket is not connected.", nameof (socket)); + CheckCanConnect (stream, host, port); - if (host == null) - throw new ArgumentNullException (nameof (host)); - - if (host.Length == 0) - throw new ArgumentException ("The host name cannot be empty.", nameof (host)); + ComputeDefaultValues (host, ref port, ref options, out var uri, out var starttls); - if (port < 0 || port > 65535) - throw new ArgumentOutOfRangeException (nameof (port)); + using var operation = engine.StartNetworkOperation (NetworkOperationKind.Connect, uri); - CheckDisposed (); + try { + Stream network; - if (IsConnected) - throw new InvalidOperationException ("The ImapClient is already connected."); - - Stream stream; - bool starttls; - Uri uri; + engine.Uri = uri; - ComputeDefaultValues (host, ref port, ref options, out uri, out starttls); + if (options == SecureSocketOptions.SslOnConnect) { + var ssl = new ExtendedSslStream (stream, false, ValidateRemoteCertificate); - engine.Uri = uri; + try { + SslHandshake (ssl, host, cancellationToken); + } catch (Exception ex) { + ssl.Dispose (); - if (options == SecureSocketOptions.SslOnConnect) { - var ssl = new SslStream (new NetworkStream (socket, true), false, ValidateRemoteCertificate); + throw SslHandshakeException.Create (ref sslValidationInfo, ex, false, "IMAP", host, port, 993, 143); + } - try { -#if NETSTANDARD - ssl.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, true).GetAwaiter ().GetResult (); -#else - ssl.AuthenticateAsClient (host, ClientCertificates, SslProtocols, true); -#endif - } catch { - ssl.Dispose (); - throw; + network = ssl; + } else { + network = stream; } - secure = true; - stream = ssl; - } else { - stream = new NetworkStream (socket, true); - secure = false; - } - - if (stream.CanTimeout) { - stream.WriteTimeout = timeout; - stream.ReadTimeout = timeout; - } - - ProtocolLogger.LogConnect (uri); - - engine.Connect (new ImapStream (stream, socket, ProtocolLogger), cancellationToken); - - try { - // Only query the CAPABILITIES if the greeting didn't include them. - if (engine.CapabilitiesVersion == 0) - engine.QueryCapabilities (cancellationToken); - - if (options == SecureSocketOptions.StartTls && (engine.Capabilities & ImapCapabilities.StartTLS) == 0) - throw new NotSupportedException ("The IMAP server does not support the STARTTLS extension."); - - if (starttls && (engine.Capabilities & ImapCapabilities.StartTLS) != 0) { - var ic = engine.QueueCommand (cancellationToken, null, "STARTTLS\r\n"); - - engine.Wait (ic); - - ProcessResponseCodes (ic); - - if (ic.Response == ImapCommandResponse.Ok) { - var tls = new SslStream (stream, false, ValidateRemoteCertificate); -#if NETSTANDARD - tls.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, true).GetAwaiter ().GetResult (); -#else - tls.AuthenticateAsClient (host, ClientCertificates, SslProtocols, true); -#endif - engine.Stream.Stream = tls; - - secure = true; - - // Query the CAPABILITIES again if the server did not include an - // untagged CAPABILITIES response to the STARTTLS command. - if (engine.CapabilitiesVersion == 1) - engine.QueryCapabilities (cancellationToken); - } else if (options == SecureSocketOptions.StartTls) { - throw ImapCommandException.Create ("STARTTLS", ic); - } + if (network.CanTimeout) { + network.WriteTimeout = timeout; + network.ReadTimeout = timeout; } - } catch { - engine.Disconnect (); - secure = false; + + PostConnect (network, host, port, options, starttls, cancellationToken); + } catch (Exception ex) { + operation.SetError (ex); throw; } - - engine.Disconnected += OnEngineDisconnected; - OnConnected (); } -#endif /// /// Disconnect the service. /// /// - /// If is true, a LOGOUT command will be issued in order to disconnect cleanly. + /// If is , a LOGOUT command will be issued in order to disconnect cleanly. /// /// - /// + /// /// - /// If set to true, a LOGOUT command will be issued in order to disconnect cleanly. + /// If set to , a LOGOUT command will be issued in order to disconnect cleanly. /// The cancellation token. /// /// The has been disposed. /// - public override void Disconnect (bool quit, CancellationToken cancellationToken = default (CancellationToken)) + public override void Disconnect (bool quit, CancellationToken cancellationToken = default) { CheckDisposed (); @@ -1571,10 +1830,7 @@ static void ComputeDefaultValues (string host, ref int port, ref SecureSocketOpt if (quit) { try { var ic = engine.QueueCommand (cancellationToken, null, "LOGOUT\r\n"); - - engine.Wait (ic); - - ProcessResponseCodes (ic); + engine.Run (ic); } catch (OperationCanceledException) { } catch (ImapProtocolException) { } catch (ImapCommandException) { @@ -1582,151 +1838,134 @@ static void ComputeDefaultValues (string host, ref int port, ref SecureSocketOpt } } -#if NETFX_CORE - socket.Dispose (); - socket = null; -#endif + disconnecting = true; + + engine.Disconnect (null); + } + + ImapCommand QueueNoOpCommand (CancellationToken cancellationToken) + { + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); + + return engine.QueueCommand (cancellationToken, null, "NOOP\r\n"); + } - engine.Disconnect (); - secure = false; + static void ProcessNoOpResponse (ImapCommand ic) + { + ic.ThrowIfNotOk ("NOOP"); } -#if ENABLE_RECONNECT /// - /// Reconnect to the most recently connected IMAP server. + /// Ping the IMAP server to keep the connection alive. /// /// - /// Reconnects to the most recently connected IMAP server. Once a - /// successful connection is made, the session will then be re-authenticated - /// using the account name used in the previous session and the - /// . + /// The NOOP command is typically used to keep the connection with the IMAP server + /// alive. When a client goes too long (typically 30 minutes) without sending any commands to the + /// IMAP server, the IMAP server will close the connection with the client, forcing the client to + /// reconnect before it can send any more commands. + /// The NOOP command also provides a great way for a client to check for new + /// messages. + /// When the IMAP server receives a NOOP command, it will reply to the client with a + /// list of pending updates such as EXISTS and RECENT counts on the currently + /// selected folder. To receive these notifications, subscribe to the + /// and events, + /// respectively. + /// For more information about the NOOP command, see + /// rfc3501. /// - /// The password. + /// + /// + /// /// The cancellation token. - /// - /// is null. - /// /// /// The has been disposed. /// /// /// The is not connected. /// - /// - /// There is no previous session to restore. + /// + /// The is not authenticated. /// /// /// The operation was canceled via the cancellation token. /// - /// - /// Authentication using the supplied credentials has failed. - /// - /// - /// A SASL authentication error occurred. - /// - /// - /// The previous session was using the STARTTLS extension but the - /// IMAP server no longer supports it. - /// /// /// An I/O error occurred. /// + /// + /// The server replied to the NOOP command with a NO or BAD response. + /// /// - /// An IMAP protocol error occurred. + /// The server responded with an unexpected token. /// - public void Reconnect (string password, CancellationToken cancellationToken = default (CancellationToken)) + public override void NoOp (CancellationToken cancellationToken = default) { - if (password == null) - throw new ArgumentNullException ("password"); + var ic = QueueNoOpCommand (cancellationToken); + + engine.Run (ic); - if (identifier == null) - throw new InvalidOperationException ("There is no previous session to restore."); + ProcessNoOpResponse (ic); + } - // Note: the identifier has the following syntax: imap(s)://userName@host:port - int startIndex = identifier.IndexOf (':') + 3; - int endIndex = identifier.IndexOf ('@'); + void CheckCanIdle (CancellationToken doneToken) + { + if (!doneToken.CanBeCanceled) + throw new ArgumentException ("The doneToken must be cancellable.", nameof (doneToken)); - var userName = UnescapeUserName (identifier.Substring (startIndex, endIndex - startIndex)); + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); - Connect (engine.Uri, cancellationToken); + if ((engine.Capabilities & ImapCapabilities.Idle) == 0) + throw new NotSupportedException ("The IMAP server does not support the IDLE extension."); - Authenticate (userName, password, cancellationToken); + if (engine.State != ImapEngineState.Selected) + throw new InvalidOperationException ("An ImapFolder has not been opened."); } - /// - /// Asynchronously reconnect to the most recently connected IMAP server. - /// - /// - /// Asynchronously reconnects to the most recently connected IMAP server. - /// Once a successful connection is made, the session will then be - /// re-authenticated using the account name used in the previous session and - /// the . - /// - /// The password. - /// The cancellation token. - /// - /// is null. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// There is no previous session to restore. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// Authentication using the supplied credentials has failed. - /// - /// - /// A SASL authentication error occurred. - /// - /// - /// The previous session was using the STARTTLS extension but the - /// IMAP server no longer supports it. - /// - /// - /// An I/O error occurred. - /// - /// - /// An IMAP protocol error occurred. - /// - public Task ReconnectAsync (string password, CancellationToken cancellationToken = default (CancellationToken)) + ImapCommand QueueIdleCommand (ImapIdleContext context, CancellationToken cancellationToken) { - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - Reconnect (password, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); + var ic = engine.QueueCommand (cancellationToken, null, "IDLE\r\n"); + ic.ContinuationHandler = context.ContinuationHandler; + ic.UserData = context; + + return ic; + } + + static void ProcessIdleResponse (ImapCommand ic) + { + ic.ThrowIfNotOk ("IDLE"); } -#endif /// - /// Ping the IMAP server to keep the connection alive. + /// Toggle the into the IDLE state. /// /// - /// The NOOP command is typically used to keep the connection with the IMAP server - /// alive. When a client goes too long (typically 30 minutes) without sending any commands to the - /// IMAP server, the IMAP server will close the connection with the client, forcing the client to - /// reconnect before it can send any more commands. - /// The NOOP command also provides a great way for a client to check for new - /// messages. - /// When the IMAP server receives a NOOP command, it will reply to the client with a - /// list of pending updates such as EXISTS and RECENT counts on the currently - /// selected folder. To receive these notifications, subscribe to the - /// and events, - /// respectively. - /// For more information about the NOOP command, see - /// rfc3501. + /// When a client enters the IDLE state, the IMAP server will send + /// events to the client as they occur on the selected folder. These events + /// may include notifications of new messages arriving, expunge notifications, + /// flag changes, etc. + /// Due to the nature of the IDLE command, a folder must be selected + /// before a client can enter into the IDLE state. This can be done by + /// opening a folder using + /// + /// or any of the other variants. + /// While the IDLE command is running, no other commands may be issued until the + /// is cancelled. + /// It is especially important to cancel the + /// before cancelling the when using SSL or TLS due to + /// the fact that cannot be polled. /// /// /// /// + /// The cancellation token used to return to the non-idle state. /// The cancellation token. + /// + /// must be cancellable (i.e. cannot be used). + /// /// /// The has been disposed. /// @@ -1736,6 +1975,12 @@ static void ComputeDefaultValues (string host, ref int port, ref SecureSocketOpt /// /// The is not authenticated. /// + /// + /// A has not been opened. + /// + /// + /// The IMAP server does not support the IDLE extension. + /// /// /// The operation was canceled via the cancellation token. /// @@ -1743,67 +1988,92 @@ static void ComputeDefaultValues (string host, ref int port, ref SecureSocketOpt /// An I/O error occurred. /// /// - /// The server replied to the NOOP command with a NO or BAD response. + /// The server replied to the IDLE command with a NO or BAD response. /// /// /// The server responded with an unexpected token. /// - public override void NoOp (CancellationToken cancellationToken = default (CancellationToken)) + public void Idle (CancellationToken doneToken, CancellationToken cancellationToken = default) + { + CheckCanIdle (doneToken); + + if (doneToken.IsCancellationRequested) + return; + + using (var context = new ImapIdleContext (engine, doneToken, cancellationToken)) { + var ic = QueueIdleCommand (context, cancellationToken); + + engine.Run (ic); + + ProcessIdleResponse (ic); + } + } + + ImapCommand QueueNotifyCommand (bool status, IList eventGroups, CancellationToken cancellationToken, out bool notifySelectedNewExpunge) { + if (eventGroups == null) + throw new ArgumentNullException (nameof (eventGroups)); + + if (eventGroups.Count == 0) + throw new ArgumentException ("No event groups specified.", nameof (eventGroups)); + CheckDisposed (); CheckConnected (); CheckAuthenticated (); - var ic = engine.QueueCommand (cancellationToken, null, "NOOP\r\n"); + if ((engine.Capabilities & ImapCapabilities.Notify) == 0) + throw new NotSupportedException ("The IMAP server does not support the NOTIFY extension."); - engine.Wait (ic); + notifySelectedNewExpunge = false; - ProcessResponseCodes (ic); + var command = new StringBuilder ("NOTIFY SET"); + var args = new List (); - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("NOOP", ic); - } + if (status) + command.Append (" STATUS"); - static void IdleComplete (object state) - { - var ctx = (ImapIdleContext) state; + foreach (var group in eventGroups) { + command.Append (' '); - if (ctx.Engine.State == ImapEngineState.Idle) { - var buf = Encoding.ASCII.GetBytes ("DONE\r\n"); + group.Format (engine, command, args, ref notifySelectedNewExpunge); + } - ctx.Engine.Stream.Write (buf, 0, buf.Length); - ctx.Engine.Stream.Flush (); + command.Append ("\r\n"); - ctx.Engine.State = ImapEngineState.Selected; - } + var ic = new ImapCommand (engine, cancellationToken, null, command.ToString (), args.ToArray ()); + + engine.QueueCommand (ic); + + return ic; + } + + void ProcessNotifyResponse (ImapCommand ic, bool notifySelectedNewExpunge) + { + ic.ThrowIfNotOk ("NOTIFY"); + + engine.NotifySelectedNewExpunge = notifySelectedNewExpunge; } /// - /// Toggle the into the IDLE state. + /// Request the specified notification events from the IMAP server. /// /// - /// When a client enters the IDLE state, the IMAP server will send - /// events to the client as they occur on the selected folder. These events - /// may include notifications of new messages arriving, expunge notifications, - /// flag changes, etc. - /// Due to the nature of the IDLE command, a folder must be selected - /// before a client can enter into the IDLE state. This can be done by - /// opening a folder using - /// - /// or any of the other variants. - /// While the IDLE command is running, no other commands may be issued until the - /// is cancelled. - /// It is especially important to cancel the - /// before cancelling the when using SSL or TLS due to - /// the fact that cannot be polled. + /// The NOTIFY command is used to expand + /// which notifications the client wishes to be notified about, including status notifications + /// about folders other than the currently selected folder. It can also be used to automatically + /// FETCH information about new messages that have arrived in the currently selected folder. + /// This, combined with , + /// can be used to get instant notifications for changes to any of the specified folders. /// - /// - /// - /// - /// The cancellation token used to return to the non-idle state. + /// if the server should immediately notify the client of the + /// selected folder's status; otherwise, . + /// The specific event groups that the client would like to receive notifications for. /// The cancellation token. + /// + /// is . + /// /// - /// must be cancellable (i.e. cannot be used). + /// is empty. /// /// /// The has been disposed. @@ -1815,10 +2085,10 @@ static void IdleComplete (object state) /// The is not authenticated. /// /// - /// A has not been opened. + /// One or more is invalid. /// /// - /// The IMAP server does not support the IDLE extension. + /// The IMAP server does not support the NOTIFY extension. /// /// /// The operation was canceled via the cancellation token. @@ -1827,70 +2097,45 @@ static void IdleComplete (object state) /// An I/O error occurred. /// /// - /// The server replied to the IDLE command with a NO or BAD response. + /// The server replied to the NOTIFY command with a NO or BAD response. /// /// /// The server responded with an unexpected token. /// - public void Idle (CancellationToken doneToken, CancellationToken cancellationToken = default (CancellationToken)) + public void Notify (bool status, IList eventGroups, CancellationToken cancellationToken = default) { - if (!doneToken.CanBeCanceled) - throw new ArgumentException ("The doneToken must be cancellable.", nameof (doneToken)); + var ic = QueueNotifyCommand (status, eventGroups, cancellationToken, out bool notifySelectedNewExpunge); + + engine.Run (ic); + + ProcessNotifyResponse (ic, notifySelectedNewExpunge); + } + ImapCommand QueueDisableNotifyCommand (CancellationToken cancellationToken) + { CheckDisposed (); CheckConnected (); CheckAuthenticated (); - if ((engine.Capabilities & ImapCapabilities.Idle) == 0) - throw new NotSupportedException ("The IMAP server does not support the IDLE extension."); - - if (engine.State != ImapEngineState.Selected) - throw new InvalidOperationException ("An ImapFolder has not been opened."); - - using (var context = new ImapIdleContext (engine, doneToken, cancellationToken)) { - var ic = engine.QueueCommand (cancellationToken, null, "IDLE\r\n"); - ic.UserData = context; - - ic.ContinuationHandler = (imap, cmd, text) => { - imap.State = ImapEngineState.Idle; + if ((engine.Capabilities & ImapCapabilities.Notify) == 0) + throw new NotSupportedException ("The IMAP server does not support the NOTIFY extension."); - doneToken.Register (IdleComplete, context); - }; + var ic = new ImapCommand (engine, cancellationToken, null, "NOTIFY NONE\r\n"); - engine.Wait (ic); - - ProcessResponseCodes (ic); + engine.QueueCommand (ic); - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("IDLE", ic); - } + return ic; } /// - /// Asynchronously toggle the into the IDLE state. + /// Disable any previously requested notification events from the IMAP server. /// /// - /// When a client enters the IDLE state, the IMAP server will send - /// events to the client as they occur on the selected folder. These events - /// may include notifications of new messages arriving, expunge notifications, - /// flag changes, etc. - /// Due to the nature of the IDLE command, a folder must be selected - /// before a client can enter into the IDLE state. This can be done by - /// opening a folder using - /// - /// or any of the other variants. - /// While the IDLE command is running, no other commands may be issued until the - /// is cancelled. - /// It is especially important to cancel the - /// before cancelling the when using SSL or TLS due to - /// the fact that cannot be polled. + /// Disables any notification events requested in a prior call to + /// . + /// request. /// - /// An asynchronous task context. - /// The cancellation token used to return to the non-idle state. /// The cancellation token. - /// - /// must be cancellable (i.e. cannot be used). - /// /// /// The has been disposed. /// @@ -1900,11 +2145,8 @@ static void IdleComplete (object state) /// /// The is not authenticated. /// - /// - /// A has not been opened. - /// /// - /// The IMAP server does not support the IDLE extension. + /// The IMAP server does not support the NOTIFY extension. /// /// /// The operation was canceled via the cancellation token. @@ -1913,31 +2155,18 @@ static void IdleComplete (object state) /// An I/O error occurred. /// /// - /// The server replied to the IDLE command with a NO or BAD response. + /// The server replied to the NOTIFY command with a NO or BAD response. /// /// /// The server responded with an unexpected token. /// - public Task IdleAsync (CancellationToken doneToken, CancellationToken cancellationToken = default (CancellationToken)) + public void DisableNotify (CancellationToken cancellationToken = default) { - if (!doneToken.CanBeCanceled) - throw new ArgumentException ("The doneToken must be cancellable.", nameof (doneToken)); - - CheckDisposed (); - CheckConnected (); - CheckAuthenticated (); - - if ((engine.Capabilities & ImapCapabilities.Idle) == 0) - throw new NotSupportedException ("The IMAP server does not support the IDLE extension."); + var ic = QueueDisableNotifyCommand (cancellationToken); - if (engine.State != ImapEngineState.Selected) - throw new InvalidOperationException ("An ImapFolder has not been opened."); + engine.Run (ic); - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - Idle (doneToken, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Default); + ProcessNotifyResponse (ic, false); } #endregion @@ -1950,6 +2179,9 @@ static void IdleComplete (object state) /// /// The personal folder namespaces contain a user's personal mailbox folders. /// + /// + /// + /// /// The personal namespaces. public override FolderNamespaceCollection PersonalNamespaces { get { return engine.PersonalNamespaces; } @@ -1961,6 +2193,9 @@ public override FolderNamespaceCollection PersonalNamespaces { /// /// The shared folder namespaces contain mailbox folders that are shared with the user. /// + /// + /// + /// /// The shared namespaces. public override FolderNamespaceCollection SharedNamespaces { get { return engine.SharedNamespaces; } @@ -1972,6 +2207,9 @@ public override FolderNamespaceCollection SharedNamespaces { /// /// The other folder namespaces contain other mailbox folders. /// + /// + /// + /// /// The other namespaces. public override FolderNamespaceCollection OtherNamespaces { get { return engine.OtherNamespaces; } @@ -1983,7 +2221,7 @@ public override FolderNamespaceCollection OtherNamespaces { /// /// Gets whether or not the mail store supports quotas. /// - /// true if the mail store supports quotas; otherwise, false. + /// if the mail store supports quotas; otherwise, . public override bool SupportsQuotas { get { return (engine.Capabilities & ImapCapabilities.Quota) != 0; } } @@ -1996,7 +2234,7 @@ public override bool SupportsQuotas { /// This property will only be available after the client has been authenticated. /// /// - /// + /// /// /// The Inbox folder. /// @@ -2014,7 +2252,7 @@ public override IMailFolder Inbox { CheckConnected (); CheckAuthenticated (); - return engine.Inbox; + return engine.Inbox!; } } @@ -2027,7 +2265,7 @@ public override IMailFolder Inbox { /// extensions may have /// special folders. /// - /// The folder if available; otherwise null. + /// The folder if available; otherwise . /// The type of special folder. /// /// is out of range. @@ -2044,7 +2282,7 @@ public override IMailFolder Inbox { /// /// The IMAP server does not support the SPECIAL-USE nor XLIST extensions. /// - public override IMailFolder GetFolder (SpecialFolder folder) + public override IMailFolder? GetFolder (SpecialFolder folder) { CheckDisposed (); CheckConnected (); @@ -2054,13 +2292,14 @@ public override IMailFolder GetFolder (SpecialFolder folder) throw new NotSupportedException ("The IMAP server does not support the SPECIAL-USE nor XLIST extensions."); switch (folder) { - case SpecialFolder.All: return engine.All; - case SpecialFolder.Archive: return engine.Archive; - case SpecialFolder.Drafts: return engine.Drafts; - case SpecialFolder.Flagged: return engine.Flagged; - case SpecialFolder.Junk: return engine.Junk; - case SpecialFolder.Sent: return engine.Sent; - case SpecialFolder.Trash: return engine.Trash; + case SpecialFolder.All: return engine.All; + case SpecialFolder.Archive: return engine.Archive; + case SpecialFolder.Drafts: return engine.Drafts; + case SpecialFolder.Flagged: return engine.Flagged; + case SpecialFolder.Important: return engine.Important; + case SpecialFolder.Junk: return engine.Junk; + case SpecialFolder.Sent: return engine.Sent; + case SpecialFolder.Trash: return engine.Trash; default: throw new ArgumentOutOfRangeException (nameof (folder)); } } @@ -2071,10 +2310,13 @@ public override IMailFolder GetFolder (SpecialFolder folder) /// /// Gets the folder for the specified namespace. /// + /// + /// + /// /// The folder. /// The namespace. /// - /// is null. + /// is . /// /// /// The has been disposed. @@ -2098,9 +2340,8 @@ public override IMailFolder GetFolder (FolderNamespace @namespace) CheckAuthenticated (); var encodedName = engine.EncodeMailboxName (@namespace.Path); - ImapFolder folder; - if (engine.GetCachedFolder (encodedName, out folder)) + if (engine.TryGetCachedFolder (encodedName, out var folder)) return folder; throw new FolderNotFoundException (@namespace.Path); @@ -2115,10 +2356,10 @@ public override IMailFolder GetFolder (FolderNamespace @namespace) /// The folders. /// The namespace. /// The status items to pre-populate. - /// If set to true, only subscribed folders will be listed. + /// If set to , only subscribed folders will be listed. /// The cancellation token. /// - /// is null. + /// is . /// /// /// The has been disposed. @@ -2144,7 +2385,7 @@ public override IMailFolder GetFolder (FolderNamespace @namespace) /// /// The server responded with an unexpected token. /// - public override IList GetFolders (FolderNamespace @namespace, StatusItems items = StatusItems.None, bool subscribedOnly = false, CancellationToken cancellationToken = default (CancellationToken)) + public override IList GetFolders (FolderNamespace @namespace, StatusItems items = StatusItems.None, bool subscribedOnly = false, CancellationToken cancellationToken = default) { if (@namespace == null) throw new ArgumentNullException (nameof (@namespace)); @@ -2153,13 +2394,7 @@ public override IMailFolder GetFolder (FolderNamespace @namespace) CheckConnected (); CheckAuthenticated (); - var folders = engine.GetFolders (@namespace, items, subscribedOnly, cancellationToken); - var list = new IMailFolder[folders.Count]; - - for (int i = 0; i < list.Length; i++) - list[i] = (IMailFolder) folders[i]; - - return list; + return engine.GetFolders (@namespace, items, subscribedOnly, cancellationToken); } /// @@ -2172,7 +2407,7 @@ public override IMailFolder GetFolder (FolderNamespace @namespace) /// The folder path. /// The cancellation token. /// - /// is null. + /// is . /// /// /// The has been disposed. @@ -2193,12 +2428,12 @@ public override IMailFolder GetFolder (FolderNamespace @namespace) /// An I/O error occurred. /// /// - /// The server replied to the IDLE command with a NO or BAD response. + /// The server replied to the LIST command with a NO or BAD response. /// /// /// The server responded with an unexpected token. /// - public override IMailFolder GetFolder (string path, CancellationToken cancellationToken = default (CancellationToken)) + public override IMailFolder GetFolder (string path, CancellationToken cancellationToken = default) { if (path == null) throw new ArgumentNullException (nameof (path)); @@ -2210,67 +2445,43 @@ public override IMailFolder GetFolder (FolderNamespace @namespace) return engine.GetFolder (path, cancellationToken); } - /// - /// Gets the specified metadata. - /// - /// - /// Gets the specified metadata. - /// - /// The requested metadata value. - /// The metadata tag. - /// The cancellation token. - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The IMAP server does not support the METADATA extension. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The server replied with a NO or BAD response. - /// - public override string GetMetadata (MetadataTag tag, CancellationToken cancellationToken = default (CancellationToken)) + ImapCommand QueueGetMetadataCommand (MetadataTag tag, CancellationToken cancellationToken) { CheckDisposed (); CheckConnected (); CheckAuthenticated (); - if ((engine.Capabilities & ImapCapabilities.Metadata) == 0) + if ((engine.Capabilities & (ImapCapabilities.Metadata | ImapCapabilities.MetadataServer)) == 0) throw new NotSupportedException ("The IMAP server does not support the METADATA extension."); var ic = new ImapCommand (engine, cancellationToken, null, "GETMETADATA \"\" %S\r\n", tag.Id); - ic.RegisterUntaggedHandler ("METADATA", ImapUtils.ParseMetadata); + ic.RegisterUntaggedHandler ("METADATA", ImapUtils.UntaggedMetadataHandler); var metadata = new MetadataCollection (); ic.UserData = metadata; engine.QueueCommand (ic); - engine.Wait (ic); - ProcessResponseCodes (ic); + return ic; + } + + string? ProcessGetMetadataResponse (ImapCommand ic, MetadataTag tag) + { + ic.ThrowIfNotOk ("GETMETADATA"); - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("GETMETADATA", ic); + var metadata = (MetadataCollection) ic.UserData!; + string? value = null; for (int i = 0; i < metadata.Count; i++) { - if (metadata [i].Tag.Id == tag.Id) - return metadata [i].Value; + if (metadata[i].EncodedName.Length == 0 && metadata[i].Tag.Id == tag.Id) { + value = metadata[i].Value; + metadata.RemoveAt (i); + break; + } } - return null; + engine.ProcessMetadataChanges (metadata); + + return value; } /// @@ -2279,15 +2490,9 @@ public override IMailFolder GetFolder (FolderNamespace @namespace) /// /// Gets the specified metadata. /// - /// The requested metadata. - /// The metadata options. - /// The metadata tags. + /// The requested metadata value. + /// The metadata tag. /// The cancellation token. - /// - /// is null. - /// -or- - /// is null. - /// /// /// The has been disposed. /// @@ -2298,7 +2503,7 @@ public override IMailFolder GetFolder (FolderNamespace @namespace) /// The is not authenticated. /// /// - /// The IMAP server does not support the METADATA extension. + /// The IMAP server does not support the METADATA or METADATA-SERVER extension. /// /// /// The operation was canceled via the cancellation token. @@ -2312,7 +2517,16 @@ public override IMailFolder GetFolder (FolderNamespace @namespace) /// /// The server replied with a NO or BAD response. /// - public override MetadataCollection GetMetadata (MetadataOptions options, IEnumerable tags, CancellationToken cancellationToken = default (CancellationToken)) + public override string? GetMetadata (MetadataTag tag, CancellationToken cancellationToken = default) + { + var ic = QueueGetMetadataCommand (tag, cancellationToken); + + engine.Run (ic); + + return ProcessGetMetadataResponse (ic, tag); + } + + bool TryQueueGetMetadataCommand (MetadataOptions options, IEnumerable tags, CancellationToken cancellationToken, [NotNullWhen (true)] out ImapCommand? ic) { if (options == null) throw new ArgumentNullException (nameof (options)); @@ -2324,8 +2538,8 @@ public override IMailFolder GetFolder (FolderNamespace @namespace) CheckConnected (); CheckAuthenticated (); - if ((engine.Capabilities & ImapCapabilities.Metadata) == 0) - throw new NotSupportedException ("The IMAP server does not support the METADATA extension."); + if ((engine.Capabilities & (ImapCapabilities.Metadata | ImapCapabilities.MetadataServer)) == 0) + throw new NotSupportedException ("The IMAP server does not support the METADATA or METADATA-SERVER extension."); var command = new StringBuilder ("GETMETADATA \"\""); var args = new List (); @@ -2333,10 +2547,16 @@ public override IMailFolder GetFolder (FolderNamespace @namespace) if (options.MaxSize.HasValue || options.Depth != 0) { command.Append (" ("); - if (options.MaxSize.HasValue) - command.AppendFormat ("MAXSIZE {0} ", options.MaxSize.Value); - if (options.Depth > 0) - command.AppendFormat ("DEPTH {0} ", options.Depth == int.MaxValue ? "infinity" : "1"); + if (options.MaxSize.HasValue) { + command.Append ("MAXSIZE "); + command.Append (options.MaxSize.Value.ToString (CultureInfo.InvariantCulture)); + command.Append (' '); + } + if (options.Depth > 0) { + command.Append ("DEPTH "); + command.Append (options.Depth == int.MaxValue ? "infinity" : "1"); + command.Append (' '); + } command[command.Length - 1] = ')'; command.Append (' '); hasOptions = true; @@ -2355,21 +2575,24 @@ public override IMailFolder GetFolder (FolderNamespace @namespace) command.Append ("\r\n"); - if (args.Count == 0) - return new MetadataCollection (); + if (args.Count == 0) { + ic = null; + return false; + } - var ic = new ImapCommand (engine, cancellationToken, null, command.ToString (), args.ToArray ()); - ic.RegisterUntaggedHandler ("METADATA", ImapUtils.ParseMetadata); + ic = new ImapCommand (engine, cancellationToken, null, command.ToString (), args.ToArray ()); + ic.RegisterUntaggedHandler ("METADATA", ImapUtils.UntaggedMetadataHandler); ic.UserData = new MetadataCollection (); options.LongEntries = 0; engine.QueueCommand (ic); - engine.Wait (ic); - ProcessResponseCodes (ic); + return true; + } - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("GETMETADATA", ic); + MetadataCollection ProcessGetMetadataResponse (ImapCommand ic, MetadataOptions options) + { + ic.ThrowIfNotOk ("GETMETADATA"); if (ic.RespCodes.Count > 0 && ic.RespCodes[ic.RespCodes.Count - 1].Type == ImapResponseCodeType.Metadata) { var metadata = (MetadataResponseCode) ic.RespCodes[ic.RespCodes.Count - 1]; @@ -2378,20 +2601,23 @@ public override IMailFolder GetFolder (FolderNamespace @namespace) options.LongEntries = metadata.Value; } - return (MetadataCollection) ic.UserData; + return engine.FilterMetadata ((MetadataCollection) ic.UserData!, string.Empty); } /// - /// Sets the specified metadata. + /// Gets the specified metadata. /// /// - /// Sets the specified metadata. + /// Gets the specified metadata. /// - /// The metadata. - /// The metadata. + /// The requested metadata. + /// The metadata options. + /// The metadata tags. /// The cancellation token. /// - /// is null. + /// is . + /// -or- + /// is . /// /// /// The has been disposed. @@ -2403,7 +2629,7 @@ public override IMailFolder GetFolder (FolderNamespace @namespace) /// The is not authenticated. /// /// - /// The IMAP server does not support the METADATA extension. + /// The IMAP server does not support the METADATA or METADATA-SERVER extension. /// /// /// The operation was canceled via the cancellation token. @@ -2417,7 +2643,17 @@ public override IMailFolder GetFolder (FolderNamespace @namespace) /// /// The server replied with a NO or BAD response. /// - public override void SetMetadata (MetadataCollection metadata, CancellationToken cancellationToken = default (CancellationToken)) + public override MetadataCollection GetMetadata (MetadataOptions options, IEnumerable tags, CancellationToken cancellationToken = default) + { + if (!TryQueueGetMetadataCommand (options, tags, cancellationToken, out var ic)) + return new MetadataCollection (); + + engine.Run (ic); + + return ProcessGetMetadataResponse (ic, options); + } + + bool TryQueueSetMetadataCommand (MetadataCollection metadata, CancellationToken cancellationToken, [NotNullWhen (true)] out ImapCommand? ic) { if (metadata == null) throw new ArgumentNullException (nameof (metadata)); @@ -2426,11 +2662,13 @@ public override IMailFolder GetFolder (FolderNamespace @namespace) CheckConnected (); CheckAuthenticated (); - if ((engine.Capabilities & ImapCapabilities.Metadata) == 0) - throw new NotSupportedException ("The IMAP server does not support the METADATA extension."); + if ((engine.Capabilities & (ImapCapabilities.Metadata | ImapCapabilities.MetadataServer)) == 0) + throw new NotSupportedException ("The IMAP server does not support the METADATA or METADATA-SERVER extension."); - if (metadata.Count == 0) - return; + if (metadata.Count == 0) { + ic = null; + return false; + } var command = new StringBuilder ("SETMETADATA \"\" ("); var args = new List (); @@ -2450,29 +2688,123 @@ public override IMailFolder GetFolder (FolderNamespace @namespace) } command.Append (")\r\n"); - var ic = new ImapCommand (engine, cancellationToken, null, command.ToString (), args.ToArray ()); + ic = new ImapCommand (engine, cancellationToken, null, command.ToString (), args.ToArray ()); engine.QueueCommand (ic); - engine.Wait (ic); - ProcessResponseCodes (ic); + return true; + } + + static void ProcessSetMetadataResponse (ImapCommand ic) + { + ic.ThrowIfNotOk ("SETMETADATA"); + } + + /// + /// Sets the specified metadata. + /// + /// + /// Sets the specified metadata. + /// + /// The metadata. + /// The cancellation token. + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The IMAP server does not support the METADATA or METADATA-SERVER extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override void SetMetadata (MetadataCollection metadata, CancellationToken cancellationToken = default) + { + if (!TryQueueSetMetadataCommand (metadata, cancellationToken, out var ic)) + return; + + engine.Run (ic); - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("SETMETADATA", ic); + ProcessSetMetadataResponse (ic); } #endregion - void OnEngineAlert (object sender, AlertEventArgs e) + void OnEngineMetadataChanged (object? sender, MetadataChangedEventArgs e) + { + OnMetadataChanged (e.Metadata); + } + + void OnEngineFolderCreated (object? sender, FolderCreatedEventArgs e) + { + OnFolderCreated (e.Folder); + } + + void OnEngineAlert (object? sender, AlertEventArgs e) { OnAlert (e.Message); } - void OnEngineDisconnected (object sender, EventArgs e) + void OnEngineWebAlert (object? sender, WebAlertEventArgs e) + { + OnWebAlert (e.WebUri, e.Message); + } + + /// + /// Occurs when a Google Mail server sends a WEBALERT response code to the client. + /// + /// + /// The event is raised whenever the Google Mail server sends a + /// WEBALERT message. + /// + public event EventHandler? WebAlert; + + /// + /// Raise the web alert event. + /// + /// + /// Raises the web alert event. + /// + /// The web alert URI. + /// The web alert message. + /// + /// is . + /// -or- + /// is . + /// + protected virtual void OnWebAlert (Uri uri, string message) + { + WebAlert?.Invoke (this, new WebAlertEventArgs (uri, message)); + } + + void OnEngineDisconnected (object? sender, EventArgs e) { - engine.Disconnected -= OnEngineDisconnected; - OnDisconnected (); - secure = false; + if (connecting) + return; + + var requested = disconnecting; + var uri = engine.Uri!; + + disconnecting = false; + + OnDisconnected (uri.Host, uri.Port, GetSecureSocketOptions (uri), requested); } /// @@ -2483,18 +2815,17 @@ void OnEngineDisconnected (object sender, EventArgs e) /// Releases the unmanaged resources used by the and /// optionally releases the managed resources. /// - /// true to release both managed and unmanaged resources; - /// false to release only the unmanaged resources. + /// to release both managed and unmanaged resources; + /// to release only the unmanaged resources. protected override void Dispose (bool disposing) { if (disposing && !disposed) { + engine.MetadataChanged -= OnEngineMetadataChanged; + engine.FolderCreated -= OnEngineFolderCreated; + engine.Disconnected -= OnEngineDisconnected; + engine.WebAlert -= OnEngineWebAlert; + engine.Alert -= OnEngineAlert; engine.Dispose (); - -#if NETFX_CORE - if (socket != null) - socket.Dispose (); -#endif - disposed = true; } diff --git a/MailKit/Net/Imap/ImapCommand.cs b/MailKit/Net/Imap/ImapCommand.cs index fea5209e65..3225a2c20f 100644 --- a/MailKit/Net/Imap/ImapCommand.cs +++ b/MailKit/Net/Imap/ImapCommand.cs @@ -1,9 +1,9 @@ -// +// // ImapCommand.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -25,21 +25,15 @@ // using System; -using System.IO; using System.Text; using System.Threading; -using System.Diagnostics; +using System.Globalization; +using System.Threading.Tasks; using System.Collections.Generic; using MimeKit; -using MimeKit.IO; using MimeKit.Utils; -#if NETFX_CORE -using Windows.Storage.Streams; -using Encoding = Portable.Text.Encoding; -#endif - namespace MailKit.Net.Imap { /// /// An IMAP continuation handler. @@ -49,7 +43,7 @@ namespace MailKit.Net.Imap { /// force-disconnect the connection. If a non-fatal error occurs, set /// it on the property. /// - delegate void ImapContinuationHandler (ImapEngine engine, ImapCommand ic, string text); + delegate Task ImapContinuationHandler (ImapEngine engine, ImapCommand ic, string text, bool doAsync); /// /// An IMAP untagged response handler. @@ -57,27 +51,10 @@ namespace MailKit.Net.Imap { /// /// Most IMAP commands return their results in untagged responses. /// - delegate void ImapUntaggedHandler (ImapEngine engine, ImapCommand ic, int index); + delegate Task ImapUntaggedHandler (ImapEngine engine, ImapCommand ic, int index, bool doAsync); delegate void ImapCommandResetHandler (ImapCommand ic); - /// - /// IMAP command status. - /// - enum ImapCommandStatus { - Created, - Queued, - Active, - Complete, - Error - } - - enum ImapLiteralType { - String, - Stream, - MimeMessage - } - enum ImapStringType { Atom, QString, @@ -85,228 +62,6 @@ enum ImapStringType { Nil } - /// - /// An IMAP IDLE context. - /// - /// - /// An IMAP IDLE command does not work like normal commands. Unlike most commands, - /// the IDLE command does not end until the client sends a separate "DONE" command. - /// In order to facilitate this, the way this works is that the consumer of MailKit's - /// IMAP APIs provides a 'doneToken' which signals to the command-processing loop to - /// send the "DONE" command. Since, like every other IMAP command, it is also necessary to - /// provide a means of cancelling the IDLE command, it becomes necessary to link the - /// 'doneToken' and the 'cancellationToken' together. - /// - sealed class ImapIdleContext : IDisposable - { - readonly CancellationTokenSource source; - - /// - /// Initializes a new instance of the class. - /// - /// - /// Creates a new . - /// - /// The IMAP engine. - /// The done token. - /// The cancellation token. - public ImapIdleContext (ImapEngine engine, CancellationToken doneToken, CancellationToken cancellationToken) - { - source = CancellationTokenSource.CreateLinkedTokenSource (doneToken, cancellationToken); - CancellationToken = cancellationToken; - DoneToken = doneToken; - Engine = engine; - } - - /// - /// Get the engine. - /// - /// - /// Gets the engine. - /// - /// The engine. - public ImapEngine Engine { - get; private set; - } - - /// - /// Get the cancellation token. - /// - /// - /// Get the cancellation token. - /// - /// The cancellation token. - public CancellationToken CancellationToken { - get; private set; - } - - /// - /// Get the linked token. - /// - /// - /// Gets the linked token. - /// - /// The linked token. - public CancellationToken LinkedToken { - get { return source.Token; } - } - - /// - /// Get the done token. - /// - /// - /// Gets the done token. - /// - /// The done token. - public CancellationToken DoneToken { - get; private set; - } - - /// - /// Get whether or not cancellation has been requested. - /// - /// - /// Gets whether or not cancellation has been requested. - /// - /// true if cancellation has been requested; otherwise, false. - public bool IsCancellationRequested { - get { return CancellationToken.IsCancellationRequested; } - } - - /// - /// Get whether or not the IDLE command should be ended. - /// - /// - /// Gets whether or not the IDLE command should be ended. - /// - /// true if the IDLE command should end; otherwise, false. - public bool IsDoneRequested { - get { return DoneToken.IsCancellationRequested; } - } - - /// - /// Releases all resource used by the object. - /// - /// Call when you are finished using the . The - /// method leaves the in an unusable state. After - /// calling , you must release all references to the - /// so the garbage collector can reclaim the memory that the - /// was occupying. - public void Dispose () - { - source.Dispose (); - } - } - - /// - /// An IMAP literal object. - /// - /// - /// The literal can be a string, byte[], Stream, or a MimeMessage. - /// - class ImapLiteral - { - public readonly ImapLiteralType Type; - public readonly object Literal; - readonly FormatOptions format; - readonly Action update; - - /// - /// Initializes a new instance of the class. - /// - /// - /// Creates a new . - /// - /// The formatting options. - /// The literal. - /// The progress update action. - public ImapLiteral (FormatOptions options, object literal, Action action = null) - { - format = options.Clone (); - format.NewLineFormat = NewLineFormat.Dos; - - update = action; - - if (literal is MimeMessage) { - Type = ImapLiteralType.MimeMessage; - } else if (literal is Stream) { - Type = ImapLiteralType.Stream; - } else if (literal is string) { - literal = Encoding.UTF8.GetBytes ((string) literal); - Type = ImapLiteralType.String; - } else if (literal is byte[]) { - Type = ImapLiteralType.String; - } else { - throw new ArgumentException ("Unknown literal type"); - } - - Literal = literal; - } - - /// - /// Get the length of the literal, in bytes. - /// - /// - /// Gets the length of the literal, in bytes. - /// - /// The length. - public long Length { - get { - if (Type == ImapLiteralType.String) - return ((byte[]) Literal).Length; - - using (var measure = new MeasuringStream ()) { - if (Type == ImapLiteralType.Stream) { - var stream = (Stream) Literal; - stream.CopyTo (measure, 4096); - stream.Position = 0; - } else { - ((MimeMessage) Literal).WriteTo (format, measure); - } - - return measure.Length; - } - } - } - - /// - /// Write the literal to the specified stream. - /// - /// - /// Writes the literal to the specified stream. - /// - /// The stream. - /// The cancellation token. - public void WriteTo (ImapStream stream, CancellationToken cancellationToken) - { - if (Type == ImapLiteralType.String) { - var bytes = (byte[]) Literal; - stream.Write (bytes, 0, bytes.Length, cancellationToken); - stream.Flush (cancellationToken); - return; - } - - if (Type == ImapLiteralType.MimeMessage) { - var message = (MimeMessage) Literal; - - using (var s = new ProgressStream (stream, update)) { - message.WriteTo (format, s, cancellationToken); - s.Flush (cancellationToken); - return; - } - } - - var literal = (Stream) Literal; - var buf = new byte[4096]; - int nread; - - while ((nread = literal.Read (buf, 0, buf.Length)) > 0) - stream.Write (buf, 0, nread, cancellationToken); - - stream.Flush (cancellationToken); - } - } - /// /// A partial IMAP command. /// @@ -318,10 +73,10 @@ public void WriteTo (ImapStream stream, CancellationToken cancellationToken) class ImapCommandPart { public readonly byte[] Command; - public readonly ImapLiteral Literal; + public readonly ImapLiteral? Literal; public readonly bool WaitForContinuation; - public ImapCommandPart (byte[] command, ImapLiteral literal, bool wait = true) + public ImapCommandPart (byte[] command, ImapLiteral? literal, bool wait = true) { WaitForContinuation = wait; Command = command; @@ -334,26 +89,33 @@ public ImapCommandPart (byte[] command, ImapLiteral literal, bool wait = true) /// class ImapCommand { - static readonly byte[] Nil = new byte[] { (byte) 'N', (byte) 'I', (byte) 'L' }; + static readonly byte[] UTF8LiteralTokenPrefix = Encoding.ASCII.GetBytes ("UTF8 (~{"); + static readonly byte[] LiteralTokenSuffix = { (byte) '}', (byte) '\r', (byte) '\n' }; + static readonly byte[] Nil = { (byte) 'N', (byte) 'I', (byte) 'L' }; + static readonly byte[] NewLine = { (byte) '\r', (byte) '\n' }; + static readonly byte[] LiteralTokenPrefix = { (byte) '{' }; public Dictionary UntaggedHandlers { get; private set; } - public ImapContinuationHandler ContinuationHandler { get; set; } + public ImapContinuationHandler? ContinuationHandler { get; set; } public CancellationToken CancellationToken { get; private set; } public ImapCommandStatus Status { get; internal set; } public ImapCommandResponse Response { get; internal set; } - public ITransferProgress Progress { get; internal set; } - public Exception Exception { get; internal set; } + public ITransferProgress? Progress { get; internal set; } + public Exception? Exception { get; internal set; } public readonly List RespCodes; - public string ResponseText { get; internal set; } - public ImapFolder Folder { get; private set; } - public object UserData { get; internal set; } - public string Tag { get; private set; } + public string? ResponseText { get; internal set; } + public ImapFolder? Folder { get; private set; } + public object? UserData { get; internal set; } + public bool ListReturnsSubscribed { get; internal set; } + public bool Logout { get; private set; } + public bool Lsub { get; internal set; } + public string? Tag { get; private set; } public bool Bye { get; internal set; } - public int Id { get; internal set; } readonly List parts = new List (); readonly ImapEngine Engine; - long totalSize, nwritten; + readonly long totalSize; + long nwritten; int current; /// @@ -368,9 +130,26 @@ class ImapCommand /// The formatting options. /// The command format. /// The command arguments. - public ImapCommand (ImapEngine engine, CancellationToken cancellationToken, ImapFolder folder, FormatOptions options, string format, params object[] args) + /// + /// is . + /// -or- + /// is . + /// -or- + /// is . + /// + public ImapCommand (ImapEngine engine, CancellationToken cancellationToken, ImapFolder? folder, FormatOptions options, string format, params object[] args) { - UntaggedHandlers = new Dictionary (); + if (engine == null) + throw new ArgumentNullException (nameof (engine)); + + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (format == null) + throw new ArgumentNullException (nameof (format)); + + UntaggedHandlers = new Dictionary (StringComparer.OrdinalIgnoreCase); + Logout = format.Equals ("LOGOUT\r\n", StringComparison.Ordinal); RespCodes = new List (); CancellationToken = cancellationToken; Response = ImapCommandResponse.None; @@ -378,62 +157,70 @@ public ImapCommand (ImapEngine engine, CancellationToken cancellationToken, Imap Engine = engine; Folder = folder; - using (var builder = new MemoryStream ()) { + using (var builder = new ByteArrayBuilder (1024)) { + byte[] buf, utf8 = new byte[8]; int argc = 0; - byte[] buf; string str; - char c; for (int i = 0; i < format.Length; i++) { if (format[i] == '%') { switch (format[++i]) { case '%': // a literal % - builder.WriteByte ((byte) '%'); - break; - case 'c': // a character - c = (char) args[argc++]; - builder.WriteByte ((byte) c); + builder.Append ((byte) '%'); break; case 'd': // an integer - str = ((int) args[argc++]).ToString (); + str = ((int) args[argc++]).ToString (CultureInfo.InvariantCulture); buf = Encoding.ASCII.GetBytes (str); - builder.Write (buf, 0, buf.Length); + builder.Append (buf, 0, buf.Length); break; case 'u': // an unsigned integer - str = ((uint) args[argc++]).ToString (); + str = ((uint) args[argc++]).ToString (CultureInfo.InvariantCulture); buf = Encoding.ASCII.GetBytes (str); - builder.Write (buf, 0, buf.Length); + builder.Append (buf, 0, buf.Length); + break; + case 's': + str = (string) args[argc++]; + buf = Encoding.ASCII.GetBytes (str); + builder.Append (buf, 0, buf.Length); break; case 'F': // an ImapFolder var utf7 = ((ImapFolder) args[argc++]).EncodedName; AppendString (options, true, builder, utf7); break; - case 'L': - var literal = new ImapLiteral (options, args[argc++], UpdateProgress); + case 'L': // a MimeMessage or a byte[] + var arg = args[argc++]; + ImapLiteral literal; + byte[] prefix; + + if (arg is MimeMessage message) { + prefix = options.International ? UTF8LiteralTokenPrefix : LiteralTokenPrefix; + literal = new ImapLiteral (options, message, UpdateProgress); + } else { + literal = new ImapLiteral (options, (byte[]) arg); + prefix = LiteralTokenPrefix; + } + var length = literal.Length; - var plus = string.Empty; bool wait = true; - if (CanUseNonSynchronizedLiteral (literal.Length)) { + builder.Append (prefix, 0, prefix.Length); + buf = Encoding.ASCII.GetBytes (length.ToString (CultureInfo.InvariantCulture)); + builder.Append (buf, 0, buf.Length); + + if (CanUseNonSynchronizedLiteral (Engine, length)) { + builder.Append ((byte) '+'); wait = false; - plus = "+"; } - totalSize += length; - - if (options.International) - str = "UTF8 (~{" + length + plus + "}\r\n"; - else - str = "{" + length + plus + "}\r\n"; + builder.Append (LiteralTokenSuffix, 0, LiteralTokenSuffix.Length); - buf = Encoding.ASCII.GetBytes (str); - builder.Write (buf, 0, buf.Length); + totalSize += length; parts.Add (new ImapCommandPart (builder.ToArray (), literal, wait)); - builder.SetLength (0); + builder.Clear (); - if (options.International) - builder.WriteByte ((byte) ')'); + if (prefix == UTF8LiteralTokenPrefix) + builder.Append ((byte) ')'); break; case 'S': // a string which may need to be quoted or made into a literal AppendString (options, true, builder, (string) args[argc++]); @@ -441,15 +228,16 @@ public ImapCommand (ImapEngine engine, CancellationToken cancellationToken, Imap case 'Q': // similar to %S but string must be quoted at a minimum AppendString (options, false, builder, (string) args[argc++]); break; - case 's': // a safe atom string - buf = Encoding.ASCII.GetBytes ((string) args[argc++]); - builder.Write (buf, 0, buf.Length); - break; default: - throw new FormatException (); + throw new FormatException ($"The %{format[i]} format specifier is not supported."); } + } else if (format[i] < 128) { + builder.Append ((byte) format[i]); } else { - builder.WriteByte ((byte) format[i]); + int nchars = char.IsSurrogate (format[i]) ? 2 : 1; + int nbytes = Encoding.UTF8.GetBytes (format, i, nchars, utf8, 0); + builder.Append (utf8, 0, nbytes); + i += nchars - 1; } } @@ -468,30 +256,126 @@ public ImapCommand (ImapEngine engine, CancellationToken cancellationToken, Imap /// The IMAP folder that the command operates on. /// The command format. /// The command arguments. - public ImapCommand (ImapEngine engine, CancellationToken cancellationToken, ImapFolder folder, string format, params object[] args) + /// + /// is . + /// -or- + /// is . + /// + public ImapCommand (ImapEngine engine, CancellationToken cancellationToken, ImapFolder? folder, string format, params object[] args) : this (engine, cancellationToken, folder, FormatOptions.Default, format, args) { } + internal static int EstimateCommandLength (ImapEngine engine, FormatOptions options, string format, params object[] args) + { + const int EstimatedTagLength = 10; + var eoln = false; + int length = 0; + int argc = 0; + string str; + + for (int i = 0; i < format.Length; i++) { + if (format[i] == '%') { + switch (format[++i]) { + //case '%': // a literal % + // Note: This is commented out because %% is only ever used in some LIST commands which never need + // to split the split the command to keep it under the max line length. + //length++; + //break; + //case 'd': // an integer + // Note: This is commented out because %d is only ever used for some REPLACE and GetMessage/GetHeaders/GetBodyPart + // commands which never need to split the command to keep it under the max line length. + //str = ((int) args[argc++]).ToString (CultureInfo.InvariantCulture); + //length += str.Length; + //break; + //case 'u': // an unsigned integer + // Note: This is commented out because %u is only ever used for some GetMessage/GetHeaders/GetBodyPart + // commands which never need to split the command to keep it under the max line length. + //str = ((uint) args[argc++]).ToString (CultureInfo.InvariantCulture); + //length += str.Length; + //break; + case 's': + str = (string) args[argc++]; + length += str.Length; + break; + case 'F': // an ImapFolder + var utf7 = ((ImapFolder) args[argc++]).EncodedName; + length += EstimateStringLength (engine, true, utf7, out eoln); + break; + //case 'L': // a MimeMessage or a byte[] + // Note: This is commented out because %L is only ever used for APPEND and REPLACE commands which + // never need to split the command to keep it under the max line length. + //var arg = args[argc++]; + //byte[] prefix; + //long len; + + //if (arg is MimeMessage message) { + // prefix = options.International ? UTF8LiteralTokenPrefix : LiteralTokenPrefix; + // var literal = new ImapLiteral (options, message, null); + // len = literal.Length; + //} else { + // len = ((byte[]) arg).Length; + // prefix = LiteralTokenPrefix; + //} + + //length += prefix.Length; + //length += Encoding.ASCII.GetByteCount (len.ToString (CultureInfo.InvariantCulture)); + + //if (CanUseNonSynchronizedLiteral (engine, len)) + // length++; + + //length += LiteralTokenSuffix.Length; + + //if (prefix == UTF8LiteralTokenPrefix) + // length++; + + //eoln = true; + //break; + case 'S': // a string which may need to be quoted or made into a literal + length += EstimateStringLength (engine, true, (string) args[argc++], out eoln); + break; + //case 'Q': // similar to %S but string must be quoted at a minimum + // Note: This is commented out because %Q is only ever used for the ID command which + // never needs to split the command to keep it under the max line length. + //length += EstimateStringLength (engine, false, (string) args[argc++], out eoln); + //break; + default: + throw new FormatException ($"The %{format[i]} format specifier is not supported."); + } + + if (eoln) + break; + } else { + length++; + } + } + + return length + EstimatedTagLength; + } + + internal static int EstimateCommandLength (ImapEngine engine, string format, params object[] args) + { + return EstimateCommandLength (engine, FormatOptions.Default, format, args); + } + void UpdateProgress (int n) { nwritten += n; - if (Progress != null) - Progress.Report (nwritten, totalSize); + Progress?.Report (nwritten, totalSize); } - static bool IsAtom (char c) + internal static bool IsAtom (char c) { - return c < 128 && !char.IsControl (c) && "(){ \t%*\\\"]".IndexOf (c) == -1; + return c < 128 && !char.IsControl (c) && "(){ %*\\\"]".IndexOf (c) == -1; } - bool IsQuotedSafe (char c) + static bool IsQuotedSafe (ImapEngine engine, char c) { - return (c < 128 || Engine.UTF8Enabled) && !char.IsControl (c); + return (c < 128 || engine.UTF8Enabled) && !char.IsControl (c); } - ImapStringType GetStringType (string value, bool allowAtom) + internal static ImapStringType GetStringType (ImapEngine engine, string value, bool allowAtom) { var type = allowAtom ? ImapStringType.Atom : ImapStringType.QString; @@ -503,7 +387,7 @@ ImapStringType GetStringType (string value, bool allowAtom) for (int i = 0; i < value.Length; i++) { if (!IsAtom (value[i])) { - if (!IsQuotedSafe (value[i])) + if (!IsQuotedSafe (engine, value[i])) return ImapStringType.Literal; type = ImapStringType.QString; @@ -513,48 +397,74 @@ ImapStringType GetStringType (string value, bool allowAtom) return type; } - bool CanUseNonSynchronizedLiteral (long length) + static bool CanUseNonSynchronizedLiteral (ImapEngine engine, long length) { - return (Engine.Capabilities & ImapCapabilities.LiteralPlus) != 0 || - (length <= 4096 && (Engine.Capabilities & ImapCapabilities.LiteralMinus) != 0); + return (engine.Capabilities & ImapCapabilities.LiteralPlus) != 0 || + (length <= 4096 && (engine.Capabilities & ImapCapabilities.LiteralMinus) != 0); + } + + static int EstimateStringLength (ImapEngine engine, bool allowAtom, string value, out bool eoln) + { + eoln = false; + + switch (GetStringType (engine, value, allowAtom)) { + case ImapStringType.Literal: + var literal = Encoding.UTF8.GetByteCount (value); + var plus = CanUseNonSynchronizedLiteral (engine, literal); + int length = "{}\r\n".Length; + + length += literal.ToString (CultureInfo.InvariantCulture).Length; + if (plus) + length++; + + eoln = true; + + return length; + case ImapStringType.QString: + return Encoding.UTF8.GetByteCount (MimeUtils.Quote (value)); + case ImapStringType.Nil: + return Nil.Length; + default: + return value.Length; + } } - void AppendString (FormatOptions options, bool allowAtom, MemoryStream builder, string value) + void AppendString (FormatOptions options, bool allowAtom, ByteArrayBuilder builder, string value) { byte[] buf; - switch (GetStringType (value, allowAtom)) { + switch (GetStringType (Engine, value, allowAtom)) { case ImapStringType.Literal: var literal = Encoding.UTF8.GetBytes (value); - var plus = CanUseNonSynchronizedLiteral (literal.Length); - var length = literal.Length.ToString (); + var plus = CanUseNonSynchronizedLiteral (Engine, literal.Length); + var length = literal.Length.ToString (CultureInfo.InvariantCulture); buf = Encoding.ASCII.GetBytes (length); - builder.WriteByte ((byte) '{'); - builder.Write (buf, 0, buf.Length); + builder.Append ((byte) '{'); + builder.Append (buf, 0, buf.Length); if (plus) - builder.WriteByte ((byte) '+'); - builder.WriteByte ((byte) '}'); - builder.WriteByte ((byte) '\r'); - builder.WriteByte ((byte) '\n'); + builder.Append ((byte) '+'); + builder.Append ((byte) '}'); + builder.Append ((byte) '\r'); + builder.Append ((byte) '\n'); if (plus) { - builder.Write (literal, 0, literal.Length); + builder.Append (literal, 0, literal.Length); } else { parts.Add (new ImapCommandPart (builder.ToArray (), new ImapLiteral (options, literal))); - builder.SetLength (0); + builder.Clear (); } break; case ImapStringType.QString: buf = Encoding.UTF8.GetBytes (MimeUtils.Quote (value)); - builder.Write (buf, 0, buf.Length); + builder.Append (buf, 0, buf.Length); break; case ImapStringType.Atom: buf = Encoding.UTF8.GetBytes (value); - builder.Write (buf, 0, buf.Length); + builder.Append (buf, 0, buf.Length); break; case ImapStringType.Nil: - builder.Write (Nil, 0, Nil.Length); + builder.Append (Nil, 0, Nil.Length); break; } } @@ -565,9 +475,9 @@ void AppendString (FormatOptions options, bool allowAtom, MemoryStream builder, /// The atom token. /// The handler. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// Untagged handlers must be registered before the command has been queued. @@ -586,9 +496,32 @@ public void RegisterUntaggedHandler (string atom, ImapUntaggedHandler handler) UntaggedHandlers.Add (atom, handler); } + static bool IsOkNoOrBad (string atom, out ImapCommandResponse response) + { + if (atom.Equals ("OK", StringComparison.OrdinalIgnoreCase)) { + response = ImapCommandResponse.Ok; + return true; + } + + if (atom.Equals ("NO", StringComparison.OrdinalIgnoreCase)) { + response = ImapCommandResponse.No; + return true; + } + + if (atom.Equals ("BAD", StringComparison.OrdinalIgnoreCase)) { + response = ImapCommandResponse.Bad; + return true; + } + + response = ImapCommandResponse.None; + + return false; + } + /// /// Sends the next part of the command to the server. /// + /// if there are more command parts to send; otherwise, . /// /// The operation was canceled via the cancellation token. /// @@ -601,31 +534,31 @@ public void RegisterUntaggedHandler (string atom, ImapUntaggedHandler handler) public bool Step () { var supportsLiteralPlus = (Engine.Capabilities & ImapCapabilities.LiteralPlus) != 0; - int timeout = Engine.Stream.CanTimeout ? Engine.Stream.ReadTimeout : -1; - var idle = UserData as ImapIdleContext; - var result = ImapCommandResponse.None; + var response = ImapCommandResponse.None; ImapToken token; // construct and write the command tag if this is the initial state if (current == 0) { - Tag = string.Format ("{0}{1:D8}", Engine.TagPrefix, Engine.Tag++); + Tag = string.Format (CultureInfo.InvariantCulture, "{0}{1:D8}", Engine.TagPrefix, Engine.Tag++); var buf = Encoding.ASCII.GetBytes (Tag + " "); - Engine.Stream.Write (buf, 0, buf.Length, CancellationToken); + + Engine.Stream!.Write (buf, 0, buf.Length, CancellationToken); } do { - var command = parts[current].Command; + var part = parts[current]; + var command = part.Command; - Engine.Stream.Write (command, 0, command.Length, CancellationToken); + Engine.Stream!.Write (command, 0, command.Length, CancellationToken); // if the server doesn't support LITERAL+ (or LITERAL-), we'll need to wait // for a "+" response before writing out the any literals... - if (parts[current].WaitForContinuation) + if (part.WaitForContinuation || part.Literal == null) break; // otherwise, we can write out any and all literal tokens we have... - parts[current].Literal.WriteTo (Engine.Stream, CancellationToken); + part.Literal.WriteTo (Engine.Stream, CancellationToken); if (current + 1 >= parts.Count) break; @@ -638,99 +571,265 @@ public bool Step () // now we need to read the response... do { if (Engine.State == ImapEngineState.Idle) { - try { - if (Engine.Stream.CanTimeout) - Engine.Stream.ReadTimeout = -1; - - token = Engine.ReadToken (idle.LinkedToken); - - if (Engine.Stream.CanTimeout) - Engine.Stream.ReadTimeout = timeout; - } catch (OperationCanceledException) { - if (Engine.Stream.CanTimeout) - Engine.Stream.ReadTimeout = timeout; + int timeout = Timeout.Infinite; - if (idle.IsCancellationRequested) - throw; - - Engine.Stream.IsConnected = true; + if (Engine.Stream.CanTimeout) { + timeout = Engine.Stream.ReadTimeout; + Engine.Stream.ReadTimeout = Timeout.Infinite; + } + try { token = Engine.ReadToken (CancellationToken); + } finally { + if (Engine.Stream != null && Engine.Stream.IsConnected && Engine.Stream.CanTimeout) + Engine.Stream.ReadTimeout = timeout; } } else { token = Engine.ReadToken (CancellationToken); } - if (token.Type == ImapTokenType.Atom && token.Value.ToString () == "+") { + if (token == ImapToken.Plus) { // we've gotten a continuation response from the server var text = Engine.ReadLine (CancellationToken).Trim (); // if we've got a Literal pending, the '+' means we can send it now... - if (!supportsLiteralPlus && parts[current].Literal != null) { - parts[current].Literal.WriteTo (Engine.Stream, CancellationToken); + var literal = parts[current].Literal; + if (!supportsLiteralPlus && literal != null) { + literal.WriteTo (Engine.Stream, CancellationToken); break; } - Debug.Assert (ContinuationHandler != null, "The ImapCommand's ContinuationHandler is null"); - - ContinuationHandler (Engine, this, text); + if (ContinuationHandler != null) { + ContinuationHandler (Engine, this, text, false); + } else { + Engine.Stream.Write (NewLine, 0, NewLine.Length, CancellationToken); + Engine.Stream.Flush (CancellationToken); + } } else if (token.Type == ImapTokenType.Asterisk) { // we got an untagged response, let the engine handle this... - Engine.ProcessUntaggedResponse (CancellationToken); + Engine.ProcessUntaggedResponse (this, CancellationToken); } else if (token.Type == ImapTokenType.Atom && (string) token.Value == Tag) { // the next token should be "OK", "NO", or "BAD" token = Engine.ReadToken (CancellationToken); - if (token.Type == ImapTokenType.Atom) { - string atom = (string) token.Value; + ImapEngine.AssertToken (token, ImapTokenType.Atom, "Syntax error in tagged response. {0}", token); - switch (atom) { - case "BAD": result = ImapCommandResponse.Bad; break; - case "OK": result = ImapCommandResponse.Ok; break; - case "NO": result = ImapCommandResponse.No; break; - default: throw ImapEngine.UnexpectedToken ("Syntax error in tagged response. Unexpected token: {0}", token); - } + string atom = (string) token.Value; - token = Engine.ReadToken (CancellationToken); - if (token.Type == ImapTokenType.OpenBracket) { - var code = Engine.ParseResponseCode (CancellationToken); - RespCodes.Add (code); - break; - } + if (!IsOkNoOrBad (atom, out response)) + throw ImapEngine.UnexpectedToken ("Syntax error in tagged response. {0}", token); - if (token.Type != ImapTokenType.Eoln) { - // consume the rest of the line... - ResponseText = ((string) (token.Value) + Engine.ReadLine (CancellationToken)).TrimEnd (); - break; - } - } else { - // looks like we didn't get an "OK", "NO", or "BAD"... - throw ImapEngine.UnexpectedToken ("Syntax error in tagged response. Unexpected token: {0}", token); + token = Engine.ReadToken (CancellationToken); + if (token.Type == ImapTokenType.OpenBracket) { + var code = Engine.ParseResponseCode (true, CancellationToken); + RespCodes.Add (code); + } else if (token.Type != ImapTokenType.Eoln) { + // consume the rest of the line... + var line = Engine.ReadLine (CancellationToken).TrimEnd (); + ResponseText = token.Value.ToString () + line; } + + var folder = Folder ?? Engine.Selected; + + folder?.FlushQueuedEvents (); + break; } else if (token.Type == ImapTokenType.OpenBracket) { // Note: this is a work-around for broken IMAP servers like Office365.com that // return RESP-CODES that are not preceded by "* OK " such as the example in // issue #115 (https://github.com/jstedfast/MailKit/issues/115). - var code = Engine.ParseResponseCode (CancellationToken); + var code = Engine.ParseResponseCode (false, CancellationToken); RespCodes.Add (code); } else { // no clue what we got... throw ImapEngine.UnexpectedToken ("Syntax error in response. Unexpected token: {0}", token); } + } while (Status == ImapCommandStatus.Active); + + if (Status == ImapCommandStatus.Active) { + current++; + + if (current >= parts.Count || response != ImapCommandResponse.None) { + Status = ImapCommandStatus.Complete; + Response = response; + return false; + } + + return true; + } + + return false; + } + + /// + /// Sends the next part of the command to the server. + /// + /// if there are more command parts to send; otherwise, . + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// An IMAP protocol error occurred. + /// + public async Task StepAsync () + { + var supportsLiteralPlus = (Engine.Capabilities & ImapCapabilities.LiteralPlus) != 0; + var response = ImapCommandResponse.None; + ImapToken token; + + // construct and write the command tag if this is the initial state + if (current == 0) { + Tag = string.Format (CultureInfo.InvariantCulture, "{0}{1:D8}", Engine.TagPrefix, Engine.Tag++); + + var buf = Encoding.ASCII.GetBytes (Tag + " "); + + await Engine.Stream!.WriteAsync (buf, 0, buf.Length, CancellationToken).ConfigureAwait (false); + } + + do { + var part = parts[current]; + var command = part.Command; + + await Engine.Stream!.WriteAsync (command, 0, command.Length, CancellationToken).ConfigureAwait (false); + + // if the server doesn't support LITERAL+ (or LITERAL-), we'll need to wait + // for a "+" response before writing out the any literals... + if (part.WaitForContinuation || part.Literal == null) + break; + + // otherwise, we can write out any and all literal tokens we have... + await part.Literal.WriteToAsync (Engine.Stream, CancellationToken).ConfigureAwait (false); + + if (current + 1 >= parts.Count) + break; + + current++; } while (true); - // the status should always be Active at this point, but just to be sure... + await Engine.Stream.FlushAsync (CancellationToken).ConfigureAwait (false); + + // now we need to read the response... + do { + if (Engine.State == ImapEngineState.Idle) { + int timeout = Timeout.Infinite; + + if (Engine.Stream.CanTimeout) { + timeout = Engine.Stream.ReadTimeout; + Engine.Stream.ReadTimeout = Timeout.Infinite; + } + + try { + token = await Engine.ReadTokenAsync (CancellationToken).ConfigureAwait (false); + } finally { + if (Engine.Stream != null && Engine.Stream.IsConnected && Engine.Stream.CanTimeout) + Engine.Stream.ReadTimeout = timeout; + } + } else { + token = await Engine.ReadTokenAsync (CancellationToken).ConfigureAwait (false); + } + + if (token == ImapToken.Plus) { + // we've gotten a continuation response from the server + var text = (await Engine.ReadLineAsync (CancellationToken).ConfigureAwait (false)).Trim (); + + // if we've got a Literal pending, the '+' means we can send it now... + var literal = parts[current].Literal; + if (!supportsLiteralPlus && literal != null) { + await literal.WriteToAsync (Engine.Stream, CancellationToken).ConfigureAwait (false); + break; + } + + if (ContinuationHandler != null) { + await ContinuationHandler (Engine, this, text, true).ConfigureAwait (false); + } else { + await Engine.Stream.WriteAsync (NewLine, 0, NewLine.Length, CancellationToken).ConfigureAwait (false); + await Engine.Stream.FlushAsync (CancellationToken).ConfigureAwait (false); + } + } else if (token.Type == ImapTokenType.Asterisk) { + // we got an untagged response, let the engine handle this... + await Engine.ProcessUntaggedResponseAsync (this, CancellationToken).ConfigureAwait (false); + } else if (token.Type == ImapTokenType.Atom && (string) token.Value == Tag) { + // the next token should be "OK", "NO", or "BAD" + token = await Engine.ReadTokenAsync (CancellationToken).ConfigureAwait (false); + + ImapEngine.AssertToken (token, ImapTokenType.Atom, "Syntax error in tagged response. {0}", token); + + string atom = (string) token.Value; + + if (!IsOkNoOrBad (atom, out response)) + throw ImapEngine.UnexpectedToken ("Syntax error in tagged response. {0}", token); + + token = await Engine.ReadTokenAsync (CancellationToken).ConfigureAwait (false); + if (token.Type == ImapTokenType.OpenBracket) { + var code = await Engine.ParseResponseCodeAsync (true, CancellationToken).ConfigureAwait (false); + RespCodes.Add (code); + } else if (token.Type != ImapTokenType.Eoln) { + // consume the rest of the line... + var line = (await Engine.ReadLineAsync (CancellationToken).ConfigureAwait (false)).TrimEnd (); + ResponseText = token.Value.ToString () + line; + } + + var folder = Folder ?? Engine.Selected; + + folder?.FlushQueuedEvents (); + break; + } else if (token.Type == ImapTokenType.OpenBracket) { + // Note: this is a work-around for broken IMAP servers like Office365.com that + // return RESP-CODES that are not preceded by "* OK " such as the example in + // issue #115 (https://github.com/jstedfast/MailKit/issues/115). + var code = await Engine.ParseResponseCodeAsync (false, CancellationToken).ConfigureAwait (false); + RespCodes.Add (code); + } else { + // no clue what we got... + throw ImapEngine.UnexpectedToken ("Syntax error in response. Unexpected token: {0}", token); + } + } while (Status == ImapCommandStatus.Active); + if (Status == ImapCommandStatus.Active) { current++; - if (current >= parts.Count || result != ImapCommandResponse.None) { + if (current >= parts.Count || response != ImapCommandResponse.None) { Status = ImapCommandStatus.Complete; - Response = result; + Response = response; return false; } + + return true; + } + + return false; + } + + /// + /// Get the first response-code of the specified type. + /// + /// + /// Gets the first response-code of the specified type. + /// + /// The type of response-code. + /// The response-code if it exists; otherwise, . + public ImapResponseCode? GetResponseCode (ImapResponseCodeType type) + { + for (int i = 0; i < RespCodes.Count; i++) { + if (RespCodes[i].Type == type) + return RespCodes[i]; } - return true; + return null; + } + + /// + /// Throw an if the response was not OK. + /// + /// + /// Throws an if the response was not OK. + /// + public void ThrowIfNotOk (string command) + { + if (Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create (command, this); } } } diff --git a/MailKit/Net/Imap/ImapCommandException.cs b/MailKit/Net/Imap/ImapCommandException.cs index 464a08a00c..00e3433f71 100644 --- a/MailKit/Net/Imap/ImapCommandException.cs +++ b/MailKit/Net/Imap/ImapCommandException.cs @@ -1,9 +1,9 @@ -// +// // ImapCommandException.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -54,9 +54,10 @@ public class ImapCommandException : CommandException /// The serialization info. /// The streaming context. /// - /// is null. + /// is . /// [SecuritySafeCritical] + [Obsolete ("This API supports obsolete formatter-based serialization. It should not be called or extended by application code.")] protected ImapCommandException (SerializationInfo info, StreamingContext context) : base (info, context) { Response = (ImapCommandResponse) info.GetValue ("Response", typeof (ImapCommandResponse)); @@ -76,7 +77,8 @@ protected ImapCommandException (SerializationInfo info, StreamingContext context internal static ImapCommandException Create (string command, ImapCommand ic) { var result = ic.Response.ToString ().ToUpperInvariant (); - string message, reason = null; + string? reason = null; + string message; if (string.IsNullOrEmpty (ic.ResponseText)) { for (int i = ic.RespCodes.Count - 1; i >= 0; i--) { @@ -85,8 +87,10 @@ internal static ImapCommandException Create (string command, ImapCommand ic) break; } } + + reason ??= string.Empty; } else { - reason = ic.ResponseText; + reason = ic.ResponseText!; } if (!string.IsNullOrEmpty (reason)) @@ -175,9 +179,12 @@ public string ResponseText { /// The serialization info. /// The streaming context. /// - /// is null. + /// is . /// [SecurityCritical] +#if NET8_0_OR_GREATER + [Obsolete ("This API supports obsolete formatter-based serialization. It should not be called or extended by application code.")] +#endif public override void GetObjectData (SerializationInfo info, StreamingContext context) { base.GetObjectData (info, context); diff --git a/MailKit/Net/Imap/ImapCommandResponse.cs b/MailKit/Net/Imap/ImapCommandResponse.cs index cc240c3302..be48e19dcb 100644 --- a/MailKit/Net/Imap/ImapCommandResponse.cs +++ b/MailKit/Net/Imap/ImapCommandResponse.cs @@ -1,9 +1,9 @@ -// +// // ImapCommandResult.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal diff --git a/MailKit/Net/Imap/ImapCommandStatus.cs b/MailKit/Net/Imap/ImapCommandStatus.cs new file mode 100644 index 0000000000..6f1aa00242 --- /dev/null +++ b/MailKit/Net/Imap/ImapCommandStatus.cs @@ -0,0 +1,39 @@ +// +// ImapCommandStatus.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +namespace MailKit.Net.Imap { + /// + /// IMAP command status. + /// + enum ImapCommandStatus + { + Created, + Queued, + Active, + Complete, + Error + } +} diff --git a/MailKit/Net/Imap/ImapEncoding.cs b/MailKit/Net/Imap/ImapEncoding.cs index 9cf6ec690c..0c11a63007 100644 --- a/MailKit/Net/Imap/ImapEncoding.cs +++ b/MailKit/Net/Imap/ImapEncoding.cs @@ -1,9 +1,9 @@ -// -// Utf7Encoding.cs +// +// ImapEncoding.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -44,7 +44,7 @@ static class ImapEncoding public static string Decode (string text) { - var decoded = new StringBuilder (); + var decoded = new StringBuilder (text.Length); bool shifted = false; int bits = 0, v = 0; int index = 0; @@ -106,7 +106,7 @@ static void Utf7ShiftOut (StringBuilder output, int u, int bits) public static string Encode (string text) { - var encoded = new StringBuilder (); + var encoded = new StringBuilder (text.Length * 2); bool shifted = false; int bits = 0, u = 0; diff --git a/MailKit/Net/Imap/ImapEngine.cs b/MailKit/Net/Imap/ImapEngine.cs index 120a03567e..b90aae423a 100644 --- a/MailKit/Net/Imap/ImapEngine.cs +++ b/MailKit/Net/Imap/ImapEngine.cs @@ -1,9 +1,9 @@ -// +// // ImapEngine.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -26,17 +26,16 @@ using System; using System.IO; +using System.Linq; using System.Text; +using System.Buffers; using System.Threading; using System.Diagnostics; +using System.Net.Security; +using System.Globalization; +using System.Threading.Tasks; using System.Collections.Generic; - -#if NETFX_CORE -using Encoding = Portable.Text.Encoding; -using EncoderExceptionFallback = Portable.Text.EncoderExceptionFallback; -using DecoderExceptionFallback = Portable.Text.DecoderExceptionFallback; -using DecoderFallbackException = Portable.Text.DecoderFallbackException; -#endif +using System.Diagnostics.CodeAnalysis; using MimeKit; @@ -53,14 +52,14 @@ enum ImapEngineState { Disconnected, /// - /// The ImapEngine is connected but has not authenticated. + /// The ImapEngine is in the process of connecting. /// - Connected, + Connecting, /// - /// The ImapEngine is in the PREAUTH state. + /// The ImapEngine is connected but not yet authenticated. /// - PreAuth, + Connected, /// /// The ImapEngine is in the authenticated state. @@ -75,13 +74,14 @@ enum ImapEngineState { /// /// The ImapEngine is in the IDLE state. /// - Idle, + Idle } enum ImapProtocolVersion { Unknown, IMAP4, - IMAP4rev1 + IMAP4rev1, + IMAP4rev2, } enum ImapUntaggedResult { @@ -91,6 +91,27 @@ enum ImapUntaggedResult { Handled } + enum ImapQuirksMode { + None, + Courier, + Cyrus, + Domino, + Dovecot, + Exchange, + Exchange2003, + Exchange2007, + GMail, + hMailServer, + iCloud, + ProtonMail, + QQMail, + SmarterMail, + UW, + Yahoo, + Yandex, + Zoho + } + class ImapFolderNameComparer : IEqualityComparer { public char DirectorySeparator; @@ -100,10 +121,10 @@ public ImapFolderNameComparer (char directorySeparator) DirectorySeparator = directorySeparator; } - public bool Equals (string x, string y) + public bool Equals (string? x, string? y) { - x = ImapUtils.CanonicalizeMailboxName (x, DirectorySeparator); - y = ImapUtils.CanonicalizeMailboxName (y, DirectorySeparator); + x = ImapUtils.CanonicalizeMailboxName (x!, DirectorySeparator); + y = ImapUtils.CanonicalizeMailboxName (y!, DirectorySeparator); return x == y; } @@ -119,47 +140,47 @@ public int GetHashCode (string obj) /// class ImapEngine : IDisposable { - internal const string GenericUntaggedResponseSyntaxErrorFormat = "Syntax error in untagged {0} response. Unexpected token: {1}"; - internal const string GenericItemSyntaxErrorFormat = "Syntax error in {0}. Unexpected token: {1}"; - const string GenericResponseCodeSyntaxErrorFormat = "Syntax error in {0} response code. Unexpected token: {1}"; - const string GreetingSyntaxErrorFormat = "Syntax error in IMAP server greeting. Unexpected token: {0}"; + internal const string GenericUntaggedResponseSyntaxErrorFormat = "Syntax error in untagged {0} response. {1}"; + internal const string GenericItemSyntaxErrorFormat = "Syntax error in {0}. {1}"; + internal const string FetchBodySyntaxErrorFormat = "Syntax error in BODY. {0}"; + const string GenericResponseCodeSyntaxErrorFormat = "Syntax error in {0} response code. {1}"; + const string GreetingSyntaxErrorFormat = "Syntax error in IMAP server greeting. {0}"; + const int BufferSize = 4096; - internal static readonly Encoding Latin1; - internal static readonly Encoding UTF8; static int TagPrefixIndex; +#if NET6_0_OR_GREATER + readonly ClientMetrics? metrics; +#endif + internal readonly Dictionary FolderCache; readonly CreateImapFolderDelegate createImapFolder; readonly ImapFolderNameComparer cacheComparer; + internal ImapQuirksMode QuirksMode; readonly List queue; + long clientConnectedTimestamp; internal char TagPrefix; - ImapCommand current; - MimeParser parser; + ImapCommand? current; + MimeParser? parser; internal int Tag; bool disposed; - int nextId; - - static ImapEngine () - { - UTF8 = Encoding.GetEncoding (65001, new EncoderExceptionFallback (), new DecoderExceptionFallback ()); - - try { - Latin1 = Encoding.GetEncoding (28591); - } catch (NotSupportedException) { - Latin1 = Encoding.GetEncoding (1252); - } - } + bool secure; public ImapEngine (CreateImapFolderDelegate createImapFolderDelegate) { +#if NET6_0_OR_GREATER + // Use the globally configured Pop3Client metrics. + metrics = Telemetry.ImapClient.Metrics; +#endif + cacheComparer = new ImapFolderNameComparer ('.'); FolderCache = new Dictionary (cacheComparer); ThreadingAlgorithms = new HashSet (); - AuthenticationMechanisms = new HashSet (); - CompressionAlgorithms = new HashSet (); - SupportedContexts = new HashSet (); - SupportedCharsets = new HashSet (); + AuthenticationMechanisms = new HashSet (StringComparer.Ordinal); + CompressionAlgorithms = new HashSet (StringComparer.Ordinal); + SupportedContexts = new HashSet (StringComparer.Ordinal); + SupportedCharsets = new HashSet (StringComparer.OrdinalIgnoreCase); Rights = new AccessRights (); PersonalNamespaces = new FolderNamespaceCollection (); @@ -169,16 +190,18 @@ public ImapEngine (CreateImapFolderDelegate createImapFolderDelegate) ProtocolVersion = ImapProtocolVersion.Unknown; createImapFolder = createImapFolderDelegate; Capabilities = ImapCapabilities.None; + QuirksMode = ImapQuirksMode.None; queue = new List (); - nextId = 1; + + TagPrefix = (char) ('A' + (TagPrefixIndex++ % 26)); } /// /// Get the authentication mechanisms supported by the IMAP server. /// /// - /// The authentication mechanisms are queried durring the - /// method. + /// The authentication mechanisms are queried during the + /// or methods. /// /// The authentication mechanisms. public HashSet AuthenticationMechanisms { @@ -190,7 +213,8 @@ public HashSet AuthenticationMechanisms { /// /// /// The compression algorithms are populated by the - /// method. + /// and + /// methods. /// /// The compression algorithms. public HashSet CompressionAlgorithms { @@ -202,7 +226,8 @@ public HashSet CompressionAlgorithms { /// /// /// The threading algorithms are populated by the - /// method. + /// and + /// methods. /// /// The threading algorithms. public HashSet ThreadingAlgorithms { @@ -235,32 +260,21 @@ public int I18NLevel { /// Get the capabilities supported by the IMAP server. /// /// - /// The capabilities will not be known until a successful connection - /// has been made via the method. + /// The capabilities will not be known until a successful connection has been + /// made via the or method. /// /// The capabilities. public ImapCapabilities Capabilities { get; set; } - /// - /// Indicates whether or not the engine is connected to a GMail server (used for various workarounds). - /// - /// - /// Indicates whether or not the engine is connected to a GMail server (used for various workarounds). - /// - /// true if the engine is connected to a GMail server; otherwise, false. - internal bool IsGMail { - get { return (Capabilities & ImapCapabilities.GMailExt1) != 0; } - } - /// /// Indicates whether or not the engine is busy processing commands. /// /// /// Indicates whether or not the engine is busy processing commands. /// - /// true if th e engine is busy processing commands; otherwise, false. + /// if th e engine is busy processing commands; otherwise, . internal bool IsBusy { get { return current != null; } } @@ -327,7 +341,7 @@ public HashSet SupportedContexts { /// /// Gets whether or not the QRESYNC feature has been enabled. /// - /// true if the QRESYNC feature has been enabled; otherwise, false. + /// if the QRESYNC feature has been enabled; otherwise, . public bool QResyncEnabled { get; internal set; } @@ -338,7 +352,7 @@ public bool QResyncEnabled { /// /// Gets whether or not the UTF8=ACCEPT feature has been enabled. /// - /// true if the UTF8=ACCEPT feature has been enabled; otherwise, false. + /// if the UTF8=ACCEPT feature has been enabled; otherwise, . public bool UTF8Enabled { get; internal set; } @@ -350,7 +364,7 @@ public bool UTF8Enabled { /// Gets the URI of the IMAP server. /// /// The URI of the IMAP server. - public Uri Uri { + public Uri? Uri { get; internal set; } @@ -361,7 +375,7 @@ public Uri Uri { /// Gets the underlying IMAP stream. /// /// The IMAP stream. - public ImapStream Stream { + public ImapStream? Stream { get; private set; } @@ -382,11 +396,37 @@ public ImapEngineState State { /// /// Gets whether or not the engine is currently connected to a IMAP server. /// - /// true if the engine is connected; otherwise, false. + /// if the engine is connected; otherwise, . + [MemberNotNullWhen (true, nameof (Stream))] public bool IsConnected { get { return Stream != null && Stream.IsConnected; } } + /// + /// Get whether or not the client is currently in the IDLE state. + /// + /// + /// Gets whether or not the client is currently in the IDLE state. + /// + /// if an IDLE command is active; otherwise, . + [MemberNotNullWhen (true, nameof (Stream))] + public bool IsIdle { + get { return IsConnected && State == ImapEngineState.Idle; } + } + + /// + /// Get whether or not the connection is secure (typically via SSL or TLS). + /// + /// + /// Gets whether or not the connection is secure (typically via SSL or TLS). + /// + /// if the connection is secure; otherwise, . + [MemberNotNullWhen (true, nameof (Stream))] + public bool IsSecure { + get { return IsConnected && secure; } + set { secure = value; } + } + /// /// Gets the personal folder namespaces. /// @@ -427,7 +467,7 @@ public FolderNamespaceCollection OtherNamespaces { /// Gets the selected folder. /// /// The selected folder. - public ImapFolder Selected { + public ImapFolder? Selected { get; internal set; } @@ -437,18 +477,29 @@ public ImapFolder Selected { /// /// Gets a value indicating whether the engine is disposed. /// - /// true if the engine is disposed; otherwise, false. + /// if the engine is disposed; otherwise, . public bool IsDisposed { get { return disposed; } } + /// + /// Gets whether the current NOTIFY status prevents using indexes and * for referencing messages. + /// + /// + /// Gets whether the current NOTIFY status prevents using indexes and * for referencing messages. This is the case when the client has asked for MessageNew or MessageExpunge events on the SELECTED mailbox. + /// + /// if the use of indexes and * is prevented; otherwise, . + internal bool NotifySelectedNewExpunge { + get; set; + } + #region Special Folders /// /// Gets the Inbox folder. /// /// The Inbox folder. - public ImapFolder Inbox { + public ImapFolder? Inbox { get; private set; } @@ -456,7 +507,7 @@ public ImapFolder Inbox { /// Gets the special folder containing an aggregate of all messages. /// /// The folder containing all messages. - public ImapFolder All { + public ImapFolder? All { get; private set; } @@ -464,7 +515,7 @@ public ImapFolder All { /// Gets the special archive folder. /// /// The archive folder. - public ImapFolder Archive { + public ImapFolder? Archive { get; private set; } @@ -472,7 +523,7 @@ public ImapFolder Archive { /// Gets the special folder containing drafts. /// /// The drafts folder. - public ImapFolder Drafts { + public ImapFolder? Drafts { get; private set; } @@ -480,7 +531,15 @@ public ImapFolder Drafts { /// Gets the special folder containing flagged messages. /// /// The flagged folder. - public ImapFolder Flagged { + public ImapFolder? Flagged { + get; private set; + } + + /// + /// Gets the special folder containing important messages. + /// + /// The important folder. + public ImapFolder? Important { get; private set; } @@ -488,7 +547,7 @@ public ImapFolder Flagged { /// Gets the special folder containing junk messages. /// /// The junk folder. - public ImapFolder Junk { + public ImapFolder? Junk { get; private set; } @@ -496,7 +555,7 @@ public ImapFolder Junk { /// Gets the special folder containing sent messages. /// /// The sent. - public ImapFolder Sent { + public ImapFolder? Sent { get; private set; } @@ -504,7 +563,7 @@ public ImapFolder Sent { /// Gets the folder containing deleted messages. /// /// The trash folder. - public ImapFolder Trash { + public ImapFolder? Trash { get; private set; } @@ -519,7 +578,69 @@ internal ImapFolder CreateImapFolder (string encodedName, FolderAttributes attri internal static ImapProtocolException UnexpectedToken (string format, params object[] args) { - return new ImapProtocolException (string.Format (format, args)) { UnexpectedToken = true }; + for (int i = 0; i < args.Length; i++) { + if (args[i] is ImapToken token) { + switch (token.Type) { + case ImapTokenType.Atom: args[i] = string.Format ("Unexpected atom token: {0}", token); break; + case ImapTokenType.Flag: args[i] = string.Format ("Unexpected flag token: {0}", token); break; + case ImapTokenType.QString: args[i] = string.Format ("Unexpected qstring token: {0}", token); break; + case ImapTokenType.Literal: args[i] = string.Format ("Unexpected literal token: {0}", token); break; + default: args[i] = string.Format ("Unexpected token: {0}", token); break; + } + break; + } + } + + return new ImapProtocolException (string.Format (CultureInfo.InvariantCulture, format, args)) { UnexpectedToken = true }; + } + + internal static void AssertToken (ImapToken token, ImapTokenType type, string format, params object[] args) + { + if (token.Type != type) + throw UnexpectedToken (format, args); + } + + internal static void AssertToken (ImapToken token, ImapTokenType type1, ImapTokenType type2, string format, params object[] args) + { + if (token.Type != type1 && token.Type != type2) + throw UnexpectedToken (format, args); + } + + internal static uint ParseNumber (ImapToken token, bool nonZero, string format, params object[] args) + { + AssertToken (token, ImapTokenType.Atom, format, args); + + // Note: Broken IMAP servers such as mail.ru sometimes incorrectly format integers as numbers with decimals and exponents. (e.g. 9.3736e+06) + // See https://github.com/jstedfast/MailKit/issues/1838 and https://github.com/jstedfast/MailKit/issues/1840 for details. + if (!uint.TryParse ((string) token.Value, NumberStyles.AllowDecimalPoint | NumberStyles.AllowExponent, CultureInfo.InvariantCulture, out var value) || (nonZero && value == 0)) + throw UnexpectedToken (format, args); + + return value; + } + + internal static ulong ParseNumber64 (ImapToken token, bool nonZero, string format, params object[] args) + { + AssertToken (token, ImapTokenType.Atom, format, args); + + if (!ulong.TryParse ((string) token.Value, NumberStyles.None, CultureInfo.InvariantCulture, out var value) || (nonZero && value == 0)) + throw UnexpectedToken (format, args); + + return value; + } + + internal static bool TryParseNumber64 (ImapToken token, out ulong value) + { + return ulong.TryParse ((string) token.Value, NumberStyles.None, CultureInfo.InvariantCulture, out value); + } + + internal static UniqueIdSet ParseUidSet (ImapToken token, uint validity, out UniqueId? minValue, out UniqueId? maxValue, string format, params object[] args) + { + AssertToken (token, ImapTokenType.Atom, format, args); + + if (!UniqueIdSet.TryParse ((string) token.Value, validity, out var uids, out minValue, out maxValue)) + throw UnexpectedToken (format, args); + + return uids; } /// @@ -531,26 +652,19 @@ internal void SetStream (ImapStream stream) Stream = stream; } - /// - /// Takes posession of the and reads the greeting. - /// - /// The IMAP stream. - /// The cancellation token - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// An IMAP protocol error occurred. - /// - public void Connect (ImapStream stream, CancellationToken cancellationToken) + public NetworkOperation StartNetworkOperation (NetworkOperationKind kind, Uri? uri = null) { - if (Stream != null) - Stream.Dispose (); +#if NET6_0_OR_GREATER + return NetworkOperation.Start (kind, uri ?? Uri!, Telemetry.ImapClient.ActivitySource, metrics); +#else + return NetworkOperation.Start (kind, uri ?? Uri!); +#endif + } - TagPrefix = (char) ('A' + (TagPrefixIndex++ % 26)); + [MemberNotNull (nameof (Stream))] + void Initialize (ImapStream stream) + { + clientConnectedTimestamp = Stopwatch.GetTimestamp (); ProtocolVersion = ImapProtocolVersion.Unknown; Capabilities = ImapCapabilities.None; AuthenticationMechanisms.Clear (); @@ -560,7 +674,10 @@ public void Connect (ImapStream stream, CancellationToken cancellationToken) SupportedContexts.Clear (); Rights.Clear (); - State = ImapEngineState.Connected; + secure = stream.Stream is SslStream; + State = ImapEngineState.Connecting; + QuirksMode = ImapQuirksMode.None; + SupportedCharsets.Add ("US-ASCII"); SupportedCharsets.Add ("UTF-8"); CapabilitiesVersion = 0; QResyncEnabled = false; @@ -570,58 +687,202 @@ public void Connect (ImapStream stream, CancellationToken cancellationToken) Stream = stream; I18NLevel = 0; Tag = 0; + } + + ImapEngineState ParseConnectedState (ImapToken token, out bool bye) + { + var atom = (string) token.Value; + + bye = false; + + if (atom.Equals ("OK", StringComparison.OrdinalIgnoreCase)) { + return ImapEngineState.Connected; + } else if (atom.Equals ("BYE", StringComparison.OrdinalIgnoreCase)) { + bye = true; + + return State; + } else if (atom.Equals ("PREAUTH", StringComparison.OrdinalIgnoreCase)) { + return ImapEngineState.Authenticated; + } else { + throw UnexpectedToken (GreetingSyntaxErrorFormat, token); + } + } + + void DetectQuirksMode (string text) + { + if (text.StartsWith ("Courier-IMAP ready.", StringComparison.Ordinal)) + QuirksMode = ImapQuirksMode.Courier; + else if (text.Contains (" Cyrus IMAP ")) + QuirksMode = ImapQuirksMode.Cyrus; + else if (text.StartsWith ("Domino IMAP4 Server", StringComparison.Ordinal)) + QuirksMode = ImapQuirksMode.Domino; + else if (text.StartsWith ("Dovecot ready.", StringComparison.Ordinal)) + QuirksMode = ImapQuirksMode.Dovecot; + else if (text.StartsWith ("Microsoft Exchange Server 2003 IMAP4rev1", StringComparison.Ordinal)) + QuirksMode = ImapQuirksMode.Exchange2003; + else if (text.StartsWith ("Microsoft Exchange Server 2007 IMAP4 service is ready", StringComparison.Ordinal)) + QuirksMode = ImapQuirksMode.Exchange2007; + else if (text.StartsWith ("The Microsoft Exchange IMAP4 service is ready.", StringComparison.Ordinal)) + QuirksMode = ImapQuirksMode.Exchange; + else if (text.StartsWith ("Gimap ready", StringComparison.Ordinal)) + QuirksMode = ImapQuirksMode.GMail; + else if (text.Contains ("QQMail ")) + QuirksMode = ImapQuirksMode.QQMail; + else if (text.StartsWith ("IMAPrev1", StringComparison.Ordinal)) // https://github.com/hmailserver/hmailserver/blob/master/hmailserver/source/Server/IMAP/IMAPConnection.cpp#L127 + QuirksMode = ImapQuirksMode.hMailServer; + else if (text.Contains (" IMAP4rev1 2007f.") || text.Contains (" Panda IMAP ")) + QuirksMode = ImapQuirksMode.UW; + else if (text.Contains ("SmarterMail")) + QuirksMode = ImapQuirksMode.SmarterMail; + else if (text.Contains ("Yandex ")) + QuirksMode = ImapQuirksMode.Yandex; + else if (text.Contains ("Zoho Mail ")) + QuirksMode = ImapQuirksMode.Zoho; + } + + /// + /// Takes possession of the and reads the greeting. + /// + /// The IMAP stream. + /// The cancellation token. + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// An IMAP protocol error occurred. + /// + public void Connect (ImapStream stream, CancellationToken cancellationToken) + { + Initialize (stream); try { - var token = stream.ReadToken (cancellationToken); + var token = ReadToken (cancellationToken); - if (token.Type != ImapTokenType.Asterisk) - throw UnexpectedToken (GreetingSyntaxErrorFormat, token); + AssertToken (token, ImapTokenType.Asterisk, GreetingSyntaxErrorFormat, token); - token = stream.ReadToken (cancellationToken); + token = ReadToken (cancellationToken); - if (token.Type != ImapTokenType.Atom) - throw UnexpectedToken (GreetingSyntaxErrorFormat, token); + AssertToken (token, ImapTokenType.Atom, GreetingSyntaxErrorFormat, token); - var atom = (string) token.Value; + var state = ParseConnectedState (token, out bool bye); + var text = string.Empty; - switch (atom) { - case "BYE": - throw new ImapProtocolException ("IMAP server unexpectedly disconnected."); - case "PREAUTH": - State = ImapEngineState.Authenticated; - break; - case "OK": - State = ImapEngineState.PreAuth; - break; - default: - throw UnexpectedToken (GreetingSyntaxErrorFormat, token); + token = ReadToken (cancellationToken); + + if (token.Type == ImapTokenType.OpenBracket) { + var code = ParseResponseCode (false, cancellationToken); + if (code.Type == ImapResponseCodeType.Alert) { + OnAlert (code.Message); + + if (bye) + throw new ImapProtocolException (code.Message); + } else { + text = code.Message; + } + } else if (token.Type != ImapTokenType.Eoln) { + text = ReadLine (cancellationToken).TrimEnd (); + text = token.Value.ToString () + text; + + if (bye) + throw new ImapProtocolException (text); + } else if (bye) { + throw new ImapProtocolException ("The IMAP server unexpectedly refused the connection."); } - token = stream.ReadToken (cancellationToken); + DetectQuirksMode (text); + + State = state; + } catch (Exception ex) { + Disconnect (ex); + throw; + } + } + + /// + /// Takes possession of the and reads the greeting. + /// + /// The IMAP stream. + /// The cancellation token. + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// An IMAP protocol error occurred. + /// + public async Task ConnectAsync (ImapStream stream, CancellationToken cancellationToken) + { + Initialize (stream); + + try { + var token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + AssertToken (token, ImapTokenType.Asterisk, GreetingSyntaxErrorFormat, token); + + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + AssertToken (token, ImapTokenType.Atom, GreetingSyntaxErrorFormat, token); + + var state = ParseConnectedState (token, out bool bye); + var text = string.Empty; + + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); if (token.Type == ImapTokenType.OpenBracket) { - var code = ParseResponseCode (cancellationToken); - if (code.Type == ImapResponseCodeType.Alert) + var code = await ParseResponseCodeAsync (false, cancellationToken).ConfigureAwait (false); + if (code.Type == ImapResponseCodeType.Alert) { OnAlert (code.Message); + + if (bye) + throw new ImapProtocolException (code.Message); + } else { + text = code.Message; + } } else if (token.Type != ImapTokenType.Eoln) { - // throw away any remaining text up until the end of the line - ReadLine (cancellationToken); + text = (await ReadLineAsync (cancellationToken).ConfigureAwait (false)).TrimEnd (); + text = token.Value.ToString () + text; + + if (bye) + throw new ImapProtocolException (text); + } else if (bye) { + throw new ImapProtocolException ("The IMAP server unexpectedly refused the connection."); } - } catch { - Disconnect (); + + DetectQuirksMode (text); + + State = state; + } catch (Exception ex) { + Disconnect (ex); throw; } } + void RecordClientDisconnected (Exception? ex) + { +#if NET6_0_OR_GREATER + metrics?.RecordClientDisconnected (clientConnectedTimestamp, Uri!, ex); +#endif + clientConnectedTimestamp = 0; + } + /// /// Disconnects the . /// /// /// Disconnects the . /// - public void Disconnect () + /// The exception that is causing the disconnection. + public void Disconnect (Exception? ex) { + RecordClientDisconnected (ex); + if (Selected != null) { + Selected.Reset (); Selected.OnClosed (); Selected = null; } @@ -633,6 +894,8 @@ public void Disconnect () Stream = null; } + secure = false; + if (State != ImapEngineState.Disconnected) { State = ImapEngineState.Disconnected; OnDisconnected (); @@ -658,33 +921,97 @@ public void Disconnect () /// public string ReadLine (CancellationToken cancellationToken) { - if (Stream == null) - throw new InvalidOperationException (); + using (var builder = new ByteArrayBuilder (64)) { + bool complete; - using (var memory = new MemoryStream ()) { - int offset, count; - byte[] buf; + do { + complete = Stream!.ReadLine (builder, cancellationToken); + } while (!complete); - while (!Stream.ReadLine (out buf, out offset, out count, cancellationToken)) - memory.Write (buf, offset, count); + // FIXME: All callers expect CRLF to be trimmed, but many also want all trailing whitespace trimmed. + builder.TrimNewLine (); - memory.Write (buf, offset, count); + return builder.ToString (); + } + } - count = (int) memory.Length; -#if !NETFX_CORE && !NETSTANDARD - buf = memory.GetBuffer (); -#else - buf = memory.ToArray (); -#endif + /// + /// Asynchronously reads a single line from the . + /// + /// The line. + /// The cancellation token. + /// + /// The engine is not connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// An IMAP protocol error occurred. + /// + public async Task ReadLineAsync (CancellationToken cancellationToken) + { + using (var builder = new ByteArrayBuilder (64)) { + bool complete; - try { - return UTF8.GetString (buf, 0, count); - } catch (DecoderFallbackException) { - return Latin1.GetString (buf, 0, count); - } + do { + complete = await Stream!.ReadLineAsync (builder, cancellationToken).ConfigureAwait (false); + } while (!complete); + + // FIXME: All callers expect CRLF to be trimmed, but many also want all trailing whitespace trimmed. + builder.TrimNewLine (); + + return builder.ToString (); } } + /// + /// Reads the next token. + /// + /// The token. + /// The cancellation token. + /// + /// The engine is not connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// An IMAP protocol error occurred. + /// + public ImapToken ReadToken (CancellationToken cancellationToken) + { + return Stream!.ReadToken (cancellationToken); + } + + /// + /// Asynchronously reads the next token. + /// + /// The token. + /// The cancellation token. + /// + /// The engine is not connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// An IMAP protocol error occurred. + /// + public ValueTask ReadTokenAsync (CancellationToken cancellationToken) + { + return Stream!.ReadTokenAsync (cancellationToken); + } + /// /// Reads the next token. /// @@ -705,13 +1032,14 @@ public string ReadLine (CancellationToken cancellationToken) /// public ImapToken ReadToken (string specials, CancellationToken cancellationToken) { - return Stream.ReadToken (specials, cancellationToken); + return Stream!.ReadToken (specials, cancellationToken); } /// - /// Reads the next token. + /// Asynchronously reads the next token. /// /// The token. + /// A list of characters that are not legal in bare string tokens. /// The cancellation token. /// /// The engine is not connected. @@ -725,9 +1053,9 @@ public ImapToken ReadToken (string specials, CancellationToken cancellationToken /// /// An IMAP protocol error occurred. /// - public ImapToken ReadToken (CancellationToken cancellationToken) + public ValueTask ReadTokenAsync (string specials, CancellationToken cancellationToken) { - return Stream.ReadToken (cancellationToken); + return Stream!.ReadTokenAsync (specials, cancellationToken); } /// @@ -750,7 +1078,7 @@ public ImapToken ReadToken (CancellationToken cancellationToken) /// public ImapToken PeekToken (string specials, CancellationToken cancellationToken) { - var token = Stream.ReadToken (specials, cancellationToken); + var token = Stream!.ReadToken (specials, cancellationToken); Stream.UngetToken (token); @@ -758,9 +1086,10 @@ public ImapToken PeekToken (string specials, CancellationToken cancellationToken } /// - /// Peeks at the next token. + /// Asynchronously peeks at the next token. /// /// The next token. + /// A list of characters that are not legal in bare string tokens. /// The cancellation token. /// /// The engine is not connected. @@ -774,9 +1103,9 @@ public ImapToken PeekToken (string specials, CancellationToken cancellationToken /// /// An IMAP protocol error occurred. /// - public ImapToken PeekToken (CancellationToken cancellationToken) + public async ValueTask PeekTokenAsync (string specials, CancellationToken cancellationToken) { - var token = Stream.ReadToken (cancellationToken); + var token = await Stream!.ReadTokenAsync (specials, cancellationToken).ConfigureAwait (false); Stream.UngetToken (token); @@ -784,12 +1113,12 @@ public ImapToken PeekToken (CancellationToken cancellationToken) } /// - /// Reads the literal as a string. + /// Peeks at the next token. /// - /// The literal. + /// The next token. /// The cancellation token. /// - /// The is not in literal mode. + /// The engine is not connected. /// /// /// The operation was canceled via the cancellation token. @@ -797,51 +1126,196 @@ public ImapToken PeekToken (CancellationToken cancellationToken) /// /// An I/O error occurred. /// - public string ReadLiteral (CancellationToken cancellationToken) + /// + /// An IMAP protocol error occurred. + /// + public ImapToken PeekToken (CancellationToken cancellationToken) + { + var token = Stream!.ReadToken (cancellationToken); + + Stream.UngetToken (token); + + return token; + } + + /// + /// Asynchronously peeks at the next token. + /// + /// The next token. + /// The cancellation token. + /// + /// The engine is not connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// An IMAP protocol error occurred. + /// + public async ValueTask PeekTokenAsync (CancellationToken cancellationToken) + { + var token = await Stream!.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + Stream.UngetToken (token); + + return token; + } + + /// + /// Unget a token. + /// + /// + /// Ungets a token. + /// + /// The token. + public void UngetToken (ImapToken token) + { + Stream!.UngetToken (token); + } + + /// + /// Reads the literal as a string. + /// + /// The literal. + /// The cancellation token. + /// + /// The is not in literal mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public string ReadLiteral (CancellationToken cancellationToken) { - if (Stream.Mode != ImapStreamMode.Literal) + if (Stream!.Mode != ImapStreamMode.Literal) throw new InvalidOperationException (); - using (var memory = new MemoryStream (Stream.LiteralLength)) { - var buf = new byte[4096]; - int nread; + int literalLength = Stream.LiteralLength; + var buf = ArrayPool.Shared.Rent (literalLength); - while ((nread = Stream.Read (buf, 0, buf.Length, cancellationToken)) > 0) - memory.Write (buf, 0, nread); + try { + int n, nread = 0; - nread = (int) memory.Length; -#if !NETFX_CORE && !NETSTANDARD - buf = memory.GetBuffer (); -#else - buf = memory.ToArray (); -#endif + do { + if ((n = Stream.Read (buf, nread, literalLength - nread, cancellationToken)) > 0) + nread += n; + } while (nread < literalLength); + + try { + return TextEncodings.UTF8.GetString (buf, 0, nread); + } catch { + return TextEncodings.Latin1.GetString (buf, 0, nread); + } + } finally { + ArrayPool.Shared.Return (buf); + } + } + + /// + /// Asynchronously reads the literal as a string. + /// + /// The literal. + /// The cancellation token. + /// + /// The is not in literal mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public async Task ReadLiteralAsync (CancellationToken cancellationToken) + { + if (Stream!.Mode != ImapStreamMode.Literal) + throw new InvalidOperationException (); + + int literalLength = Stream.LiteralLength; + var buf = ArrayPool.Shared.Rent (literalLength); + + try { + int n, nread = 0; - return Latin1.GetString (buf, 0, nread); + do { + if ((n = await Stream.ReadAsync (buf, nread, literalLength - nread, cancellationToken).ConfigureAwait (false)) > 0) + nread += n; + } while (nread < literalLength); + + try { + return TextEncodings.UTF8.GetString (buf, 0, nread); + } catch { + return TextEncodings.Latin1.GetString (buf, 0, nread); + } + } finally { + ArrayPool.Shared.Return (buf); } } - internal void SkipLine (CancellationToken cancellationToken) + void SkipLine (CancellationToken cancellationToken) { ImapToken token; do { - token = Stream.ReadToken (cancellationToken); + token = ReadToken (cancellationToken); if (token.Type == ImapTokenType.Literal) { - var buf = new byte[4096]; + var buf = ArrayPool.Shared.Rent (BufferSize); int nread; - do { - nread = Stream.Read (buf, 0, buf.Length, cancellationToken); - } while (nread > 0); + try { + do { + nread = Stream!.Read (buf, 0, BufferSize, cancellationToken); + } while (nread > 0); + } finally { + ArrayPool.Shared.Return (buf); + } } } while (token.Type != ImapTokenType.Eoln); } - void UpdateCapabilities (ImapTokenType sentinel, CancellationToken cancellationToken) + async Task SkipLineAsync (CancellationToken cancellationToken) + { + ImapToken token; + + do { + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.Literal) { + var buf = ArrayPool.Shared.Rent (BufferSize); + int nread; + + try { + do { + nread = await Stream!.ReadAsync (buf, 0, BufferSize, cancellationToken).ConfigureAwait (false); + } while (nread > 0); + } finally { + ArrayPool.Shared.Return (buf); + } + } + } while (token.Type != ImapTokenType.Eoln); + } + + static bool TryParseUInt32 (string text, int startIndex, out uint value) + { +#if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER + var token = text.AsSpan (startIndex); +#else + var token = text.Substring (startIndex); +#endif + + return uint.TryParse (token, NumberStyles.None, CultureInfo.InvariantCulture, out value); + } + + void ResetCapabilities () { + // Clear the extensions except STARTTLS so that this capability stays set after a STARTTLS command. ProtocolVersion = ImapProtocolVersion.Unknown; - Capabilities = ImapCapabilities.None; + Capabilities &= ImapCapabilities.StartTLS; AuthenticationMechanisms.Clear (); CompressionAlgorithms.Clear (); ThreadingAlgorithms.Clear (); @@ -850,112 +1324,177 @@ void UpdateCapabilities (ImapTokenType sentinel, CancellationToken cancellationT AppendLimit = null; Rights.Clear (); I18NLevel = 0; + } - var token = Stream.ReadToken (cancellationToken); - - while (token.Type == ImapTokenType.Atom) { - var atom = (string) token.Value; - - if (atom.StartsWith ("AUTH=", StringComparison.Ordinal)) { - AuthenticationMechanisms.Add (atom.Substring ("AUTH=".Length)); - } else if (atom.StartsWith ("APPENDLIMIT=", StringComparison.Ordinal)) { - uint limit; - - if (uint.TryParse (atom.Substring ("APPENDLIMIT=".Length), out limit)) - AppendLimit = limit; + void ProcessCapabilityToken (string atom) + { + if (atom.StartsWith ("AUTH=", StringComparison.OrdinalIgnoreCase)) { + AuthenticationMechanisms.Add (atom.Substring ("AUTH=".Length)); + } else if (atom.StartsWith ("APPENDLIMIT", StringComparison.OrdinalIgnoreCase)) { + if (atom.Length >= "APPENDLIMIT".Length) { + if (atom.Length >= "APPENDLIMIT=".Length && TryParseUInt32 (atom, "APPENDLIMIT=".Length, out uint limit)) + AppendLimit = limit; Capabilities |= ImapCapabilities.AppendLimit; - } else if (atom.StartsWith ("COMPRESS=", StringComparison.Ordinal)) { - CompressionAlgorithms.Add (atom.Substring ("COMPRESS=".Length)); - Capabilities |= ImapCapabilities.Compress; - } else if (atom.StartsWith ("CONTEXT=", StringComparison.Ordinal)) { - SupportedContexts.Add (atom.Substring ("CONTEXT=".Length)); - Capabilities |= ImapCapabilities.Context; - } else if (atom.StartsWith ("I18NLEVEL=", StringComparison.Ordinal)) { - int level; - - int.TryParse (atom.Substring ("I18NLEVEL=".Length), out level); - I18NLevel = level; - - Capabilities |= ImapCapabilities.I18NLevel; - } else if (atom.StartsWith ("RIGHTS=", StringComparison.Ordinal)) { - var rights = atom.Substring ("RIGHTS=".Length); - Rights.AddRange (rights); - } else if (atom.StartsWith ("THREAD=", StringComparison.Ordinal)) { - var algorithm = atom.Substring ("THREAD=".Length); - switch (algorithm) { - case "ORDEREDSUBJECT": - ThreadingAlgorithms.Add (ThreadingAlgorithm.OrderedSubject); - break; - case "REFERENCES": - ThreadingAlgorithms.Add (ThreadingAlgorithm.References); - break; - } - - Capabilities |= ImapCapabilities.Thread; - } else { - switch (atom.ToUpperInvariant ()) { - case "IMAP4": Capabilities |= ImapCapabilities.IMAP4; break; - case "IMAP4REV1": Capabilities |= ImapCapabilities.IMAP4rev1; break; - case "STATUS": Capabilities |= ImapCapabilities.Status; break; - case "ACL": Capabilities |= ImapCapabilities.Acl; break; - case "QUOTA": Capabilities |= ImapCapabilities.Quota; break; - case "LITERAL+": Capabilities |= ImapCapabilities.LiteralPlus; break; - case "IDLE": Capabilities |= ImapCapabilities.Idle; break; - case "MAILBOX-REFERRALS": Capabilities |= ImapCapabilities.MailboxReferrals; break; - case "LOGIN-REFERRALS": Capabilities |= ImapCapabilities.LoginReferrals; break; - case "NAMESPACE": Capabilities |= ImapCapabilities.Namespace; break; - case "ID": Capabilities |= ImapCapabilities.Id; break; - case "CHILDREN": Capabilities |= ImapCapabilities.Children; break; - case "LOGINDISABLED": Capabilities |= ImapCapabilities.LoginDisabled; break; - case "STARTTLS": Capabilities |= ImapCapabilities.StartTLS; break; - case "MULTIAPPEND": Capabilities |= ImapCapabilities.MultiAppend; break; - case "BINARY": Capabilities |= ImapCapabilities.Binary; break; - case "UNSELECT": Capabilities |= ImapCapabilities.Unselect; break; - case "UIDPLUS": Capabilities |= ImapCapabilities.UidPlus; break; - case "CATENATE": Capabilities |= ImapCapabilities.Catenate; break; - case "CONDSTORE": Capabilities |= ImapCapabilities.CondStore; break; - case "ESEARCH": Capabilities |= ImapCapabilities.ESearch; break; - case "SASL-IR": Capabilities |= ImapCapabilities.SaslIR; break; - case "WITHIN": Capabilities |= ImapCapabilities.Within; break; - case "ENABLE": Capabilities |= ImapCapabilities.Enable; break; - case "QRESYNC": Capabilities |= ImapCapabilities.QuickResync; break; - case "SEARCHRES": Capabilities |= ImapCapabilities.SearchResults; break; - case "SORT": Capabilities |= ImapCapabilities.Sort; break; - case "LIST-EXTENDED": Capabilities |= ImapCapabilities.ListExtended; break; - case "CONVERT": Capabilities |= ImapCapabilities.Convert; break; - case "LANGUAGE": Capabilities |= ImapCapabilities.Language; break; - case "ESORT": Capabilities |= ImapCapabilities.ESort; break; - case "METADATA": Capabilities |= ImapCapabilities.Metadata; break; - case "NOTIFY": Capabilities |= ImapCapabilities.Notify; break; - case "LIST-STATUS": Capabilities |= ImapCapabilities.ListStatus; break; - case "SORT=DISPLAY": Capabilities |= ImapCapabilities.SortDisplay; break; - case "CREATE-SPECIAL-USE": Capabilities |= ImapCapabilities.CreateSpecialUse; break; - case "SPECIAL-USE": Capabilities |= ImapCapabilities.SpecialUse; break; - case "SEARCH=FUZZY": Capabilities |= ImapCapabilities.FuzzySearch; break; - case "MULTISEARCH": Capabilities |= ImapCapabilities.MultiSearch; break; - case "MOVE": Capabilities |= ImapCapabilities.Move; break; - case "UTF8=ACCEPT": Capabilities |= ImapCapabilities.UTF8Accept; break; - case "UTF8=ONLY": Capabilities |= ImapCapabilities.UTF8Only; break; - case "LITERAL-": Capabilities |= ImapCapabilities.LiteralMinus; break; - case "APPENDLIMIT": Capabilities |= ImapCapabilities.AppendLimit; break; - case "XLIST": Capabilities |= ImapCapabilities.XList; break; - case "X-GM-EXT-1": Capabilities |= ImapCapabilities.GMailExt1; break; - } } - - token = Stream.ReadToken (cancellationToken); - } - - if (token.Type != sentinel) { - Debug.WriteLine ("Expected '{0}' at the end of the CAPABILITIES, but got: {1}", sentinel, token); - throw UnexpectedToken (GenericItemSyntaxErrorFormat, "CAPABILITIES", token); + } else if (atom.StartsWith ("COMPRESS=", StringComparison.OrdinalIgnoreCase)) { + CompressionAlgorithms.Add (atom.Substring ("COMPRESS=".Length)); + Capabilities |= ImapCapabilities.Compress; + } else if (atom.StartsWith ("CONTEXT=", StringComparison.OrdinalIgnoreCase)) { + SupportedContexts.Add (atom.Substring ("CONTEXT=".Length)); + Capabilities |= ImapCapabilities.Context; + } else if (atom.StartsWith ("I18NLEVEL=", StringComparison.OrdinalIgnoreCase)) { + if (TryParseUInt32 (atom, "I18NLEVEL=".Length, out uint level)) + I18NLevel = (int) level; + + Capabilities |= ImapCapabilities.I18NLevel; + } else if (atom.StartsWith ("RIGHTS=", StringComparison.OrdinalIgnoreCase)) { + var rights = atom.Substring ("RIGHTS=".Length); + Rights.AddRange (rights); + } else if (atom.StartsWith ("THREAD=", StringComparison.OrdinalIgnoreCase)) { + if (string.Compare ("ORDEREDSUBJECT", 0, atom, "THREAD=".Length, "ORDEREDSUBJECT".Length, StringComparison.OrdinalIgnoreCase) == 0) + ThreadingAlgorithms.Add (ThreadingAlgorithm.OrderedSubject); + else if (string.Compare ("REFERENCES", 0, atom, "THREAD=".Length, "REFERENCES".Length, StringComparison.OrdinalIgnoreCase) == 0) + ThreadingAlgorithms.Add (ThreadingAlgorithm.References); + + Capabilities |= ImapCapabilities.Thread; + } else if (atom.Equals ("IMAP4", StringComparison.OrdinalIgnoreCase)) { + Capabilities |= ImapCapabilities.IMAP4; + } else if (atom.Equals ("IMAP4REV1", StringComparison.OrdinalIgnoreCase)) { + Capabilities |= ImapCapabilities.IMAP4rev1; + } else if (atom.Equals ("IMAP4REV2", StringComparison.OrdinalIgnoreCase)) { + Capabilities |= ImapCapabilities.IMAP4rev2; + } else if (atom.Equals ("STATUS", StringComparison.OrdinalIgnoreCase)) { + Capabilities |= ImapCapabilities.Status; + } else if (atom.Equals ("ACL", StringComparison.OrdinalIgnoreCase)) { + Capabilities |= ImapCapabilities.Acl; + } else if (atom.Equals ("QUOTA", StringComparison.OrdinalIgnoreCase)) { + Capabilities |= ImapCapabilities.Quota; + } else if (atom.Equals ("LITERAL+", StringComparison.OrdinalIgnoreCase)) { + Capabilities |= ImapCapabilities.LiteralPlus; + } else if (atom.Equals ("IDLE", StringComparison.OrdinalIgnoreCase)) { + Capabilities |= ImapCapabilities.Idle; + } else if (atom.Equals ("MAILBOX-REFERRALS", StringComparison.OrdinalIgnoreCase)) { + Capabilities |= ImapCapabilities.MailboxReferrals; + } else if (atom.Equals ("LOGIN-REFERRALS", StringComparison.OrdinalIgnoreCase)) { + Capabilities |= ImapCapabilities.LoginReferrals; + } else if (atom.Equals ("NAMESPACE", StringComparison.OrdinalIgnoreCase)) { + Capabilities |= ImapCapabilities.Namespace; + } else if (atom.Equals ("ID", StringComparison.OrdinalIgnoreCase)) { + Capabilities |= ImapCapabilities.Id; + } else if (atom.Equals ("CHILDREN", StringComparison.OrdinalIgnoreCase)) { + Capabilities |= ImapCapabilities.Children; + } else if (atom.Equals ("LOGINDISABLED", StringComparison.OrdinalIgnoreCase)) { + Capabilities |= ImapCapabilities.LoginDisabled; + } else if (atom.Equals ("STARTTLS", StringComparison.OrdinalIgnoreCase)) { + Capabilities |= ImapCapabilities.StartTLS; + } else if (atom.Equals ("MULTIAPPEND", StringComparison.OrdinalIgnoreCase)) { + Capabilities |= ImapCapabilities.MultiAppend; + } else if (atom.Equals ("BINARY", StringComparison.OrdinalIgnoreCase)) { + Capabilities |= ImapCapabilities.Binary; + } else if (atom.Equals ("UNSELECT", StringComparison.OrdinalIgnoreCase)) { + Capabilities |= ImapCapabilities.Unselect; + } else if (atom.Equals ("UIDPLUS", StringComparison.OrdinalIgnoreCase)) { + Capabilities |= ImapCapabilities.UidPlus; + } else if (atom.Equals ("CATENATE", StringComparison.OrdinalIgnoreCase)) { + Capabilities |= ImapCapabilities.Catenate; + } else if (atom.Equals ("CONDSTORE", StringComparison.OrdinalIgnoreCase)) { + Capabilities |= ImapCapabilities.CondStore; + } else if (atom.Equals ("ESEARCH", StringComparison.OrdinalIgnoreCase)) { + Capabilities |= ImapCapabilities.ESearch; + } else if (atom.Equals ("SASL-IR", StringComparison.OrdinalIgnoreCase)) { + Capabilities |= ImapCapabilities.SaslIR; + } else if (atom.Equals ("WITHIN", StringComparison.OrdinalIgnoreCase)) { + Capabilities |= ImapCapabilities.Within; + } else if (atom.Equals ("ENABLE", StringComparison.OrdinalIgnoreCase)) { + Capabilities |= ImapCapabilities.Enable; + } else if (atom.Equals ("QRESYNC", StringComparison.OrdinalIgnoreCase)) { + Capabilities |= ImapCapabilities.QuickResync; + } else if (atom.Equals ("SEARCHRES", StringComparison.OrdinalIgnoreCase)) { + Capabilities |= ImapCapabilities.SearchResults; + } else if (atom.Equals ("SORT", StringComparison.OrdinalIgnoreCase)) { + Capabilities |= ImapCapabilities.Sort; + } else if (atom.Equals ("ANNOTATE-EXPERIMENT-1", StringComparison.OrdinalIgnoreCase)) { + Capabilities |= ImapCapabilities.Annotate; + } else if (atom.Equals ("LIST-EXTENDED", StringComparison.OrdinalIgnoreCase)) { + Capabilities |= ImapCapabilities.ListExtended; + } else if (atom.Equals ("CONVERT", StringComparison.OrdinalIgnoreCase)) { + Capabilities |= ImapCapabilities.Convert; + } else if (atom.Equals ("LANGUAGE", StringComparison.OrdinalIgnoreCase)) { + Capabilities |= ImapCapabilities.Language; + } else if (atom.Equals ("ESORT", StringComparison.OrdinalIgnoreCase)) { + Capabilities |= ImapCapabilities.ESort; + } else if (atom.Equals ("METADATA", StringComparison.OrdinalIgnoreCase)) { + Capabilities |= ImapCapabilities.Metadata; + } else if (atom.Equals ("METADATA-SERVER", StringComparison.OrdinalIgnoreCase)) { + Capabilities |= ImapCapabilities.MetadataServer; + } else if (atom.Equals ("NOTIFY", StringComparison.OrdinalIgnoreCase)) { + Capabilities |= ImapCapabilities.Notify; + } else if (atom.Equals ("FILTERS", StringComparison.OrdinalIgnoreCase)) { + Capabilities |= ImapCapabilities.Filters; + } else if (atom.Equals ("LIST-STATUS", StringComparison.OrdinalIgnoreCase)) { + Capabilities |= ImapCapabilities.ListStatus; + } else if (atom.Equals ("SORT=DISPLAY", StringComparison.OrdinalIgnoreCase)) { + Capabilities |= ImapCapabilities.SortDisplay; + } else if (atom.Equals ("CREATE-SPECIAL-USE", StringComparison.OrdinalIgnoreCase)) { + Capabilities |= ImapCapabilities.CreateSpecialUse; + } else if (atom.Equals ("SPECIAL-USE", StringComparison.OrdinalIgnoreCase)) { + Capabilities |= ImapCapabilities.SpecialUse; + } else if (atom.Equals ("SEARCH=FUZZY", StringComparison.OrdinalIgnoreCase)) { + Capabilities |= ImapCapabilities.FuzzySearch; + } else if (atom.Equals ("MULTISEARCH", StringComparison.OrdinalIgnoreCase)) { + Capabilities |= ImapCapabilities.MultiSearch; + } else if (atom.Equals ("MOVE", StringComparison.OrdinalIgnoreCase)) { + Capabilities |= ImapCapabilities.Move; + } else if (atom.Equals ("UTF8=ACCEPT", StringComparison.OrdinalIgnoreCase)) { + Capabilities |= ImapCapabilities.UTF8Accept; + } else if (atom.Equals ("UTF8=ONLY", StringComparison.OrdinalIgnoreCase)) { + Capabilities |= ImapCapabilities.UTF8Only; + } else if (atom.Equals ("LITERAL-", StringComparison.OrdinalIgnoreCase)) { + Capabilities |= ImapCapabilities.LiteralMinus; + } else if (atom.Equals ("UNAUTHENTICATE", StringComparison.OrdinalIgnoreCase)) { + Capabilities |= ImapCapabilities.Unauthenticate; + } else if (atom.Equals ("STATUS=SIZE", StringComparison.OrdinalIgnoreCase)) { + Capabilities |= ImapCapabilities.StatusSize; + } else if (atom.Equals ("LIST-MYRIGHTS", StringComparison.OrdinalIgnoreCase)) { + Capabilities |= ImapCapabilities.ListMyRights; + } else if (atom.Equals ("OBJECTID", StringComparison.OrdinalIgnoreCase)) { + Capabilities |= ImapCapabilities.ObjectID; + } else if (atom.Equals ("REPLACE", StringComparison.OrdinalIgnoreCase)) { + Capabilities |= ImapCapabilities.Replace; + } else if (atom.Equals ("SAVEDATE", StringComparison.OrdinalIgnoreCase)) { + Capabilities |= ImapCapabilities.SaveDate; + } else if (atom.Equals ("PREVIEW", StringComparison.OrdinalIgnoreCase)) { + Capabilities |= ImapCapabilities.Preview; + } else if (atom.Equals ("XLIST", StringComparison.OrdinalIgnoreCase)) { + Capabilities |= ImapCapabilities.XList; + } else if (atom.Equals ("X-GM-EXT-1", StringComparison.OrdinalIgnoreCase)) { + Capabilities |= ImapCapabilities.GMailExt1; + QuirksMode = ImapQuirksMode.GMail; + } else if (atom.Equals ("XSTOP", StringComparison.OrdinalIgnoreCase)) { + QuirksMode = ImapQuirksMode.ProtonMail; + } else if (atom.Equals ("XAPPLEPUSHSERVICE", StringComparison.OrdinalIgnoreCase)) { + QuirksMode = ImapQuirksMode.iCloud; + } else if (atom.Equals ("XYMHIGHESTMODSEQ", StringComparison.OrdinalIgnoreCase)) { + QuirksMode = ImapQuirksMode.Yahoo; } + } - // unget the sentinel - Stream.UngetToken (token); - - if ((Capabilities & ImapCapabilities.IMAP4rev1) != 0) { + void StandardizeCapabilities () + { + if ((Capabilities & ImapCapabilities.IMAP4rev2) != 0) { + ProtocolVersion = ImapProtocolVersion.IMAP4rev2; + + // Rfc9051, Appendix E defines the capabilities that IMAP4rev2 should be assumed to implement: + Capabilities |= ImapCapabilities.Status | + ImapCapabilities.Namespace | ImapCapabilities.Unselect | ImapCapabilities.UidPlus | ImapCapabilities.ESearch | + ImapCapabilities.SearchResults | ImapCapabilities.Enable | ImapCapabilities.Idle | ImapCapabilities.SaslIR | ImapCapabilities.ListExtended | + ImapCapabilities.ListStatus | ImapCapabilities.Move | ImapCapabilities.LiteralMinus | ImapCapabilities.SpecialUse; + + // Note: IMAP4rev2 also supports the FETCH portion of the 'BINARY' extension but not the APPEND portion. Since + // we currently have no way to distinguish between them using the ImapCapabilities enum, we do not enable the + // ImapCapabilities.Binary extension flag. + } else if ((Capabilities & ImapCapabilities.IMAP4rev1) != 0) { ProtocolVersion = ImapProtocolVersion.IMAP4rev1; Capabilities |= ImapCapabilities.Status; } else if ((Capabilities & ImapCapabilities.IMAP4) != 0) { @@ -969,12 +1508,65 @@ void UpdateCapabilities (ImapTokenType sentinel, CancellationToken cancellationT Capabilities |= ImapCapabilities.UTF8Accept; } + void UpdateCapabilities (ImapTokenType sentinel, CancellationToken cancellationToken) + { + ResetCapabilities (); + + var token = ReadToken (cancellationToken); + + // Note: Some buggy IMAP servers mistakenly put a space between "LITERAL" and "+" which causes our tokenizer to read + // a '+' token thereby causing an "Unexpected token: '+'" exception to be thrown. If we treat '+' tokens as atoms + // like we did in v4.1.0 (and older), then we can avoid this exception. + // + // See https://github.com/jstedfast/MailKit/issues/1654 for details. + while (token.Type == ImapTokenType.Atom) { + var atom = token.Value.ToString (); + + ProcessCapabilityToken (atom!); + + token = ReadToken (cancellationToken); + } + + AssertToken (token, sentinel, GenericItemSyntaxErrorFormat, "CAPABILITIES", token); + + // unget the sentinel + UngetToken (token); + + StandardizeCapabilities (); + } + + async Task UpdateCapabilitiesAsync (ImapTokenType sentinel, CancellationToken cancellationToken) + { + ResetCapabilities (); + + var token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + // Note: Some buggy IMAP servers mistakenly put a space between "LITERAL" and "+" which causes our tokenizer to read + // a '+' token thereby causing an "Unexpected token: '+'" exception to be thrown. If we treat '+' tokens as atoms + // like we did in v4.1.0 (and older), then we can avoid this exception. + // + // See https://github.com/jstedfast/MailKit/issues/1654 for details. + while (token.Type == ImapTokenType.Atom) { + var atom = token.Value.ToString (); + + ProcessCapabilityToken (atom!); + + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + } + + AssertToken (token, sentinel, GenericItemSyntaxErrorFormat, "CAPABILITIES", token); + + // unget the sentinel + UngetToken (token); + + StandardizeCapabilities (); + } + void UpdateNamespaces (CancellationToken cancellationToken) { var namespaces = new List { - PersonalNamespaces, SharedNamespaces, OtherNamespaces + PersonalNamespaces, OtherNamespaces, SharedNamespaces }; - ImapFolder folder; ImapToken token; string path; char delim; @@ -984,31 +1576,25 @@ void UpdateNamespaces (CancellationToken cancellationToken) SharedNamespaces.Clear (); OtherNamespaces.Clear (); - token = Stream.ReadToken (cancellationToken); + token = ReadToken (cancellationToken); do { if (token.Type == ImapTokenType.OpenParen) { // parse the list of namespace pairs... - token = Stream.ReadToken (cancellationToken); + token = ReadToken (cancellationToken); while (token.Type == ImapTokenType.OpenParen) { // parse the namespace pair - first token is the path - token = Stream.ReadToken (cancellationToken); + token = ReadToken (cancellationToken); - if (token.Type != ImapTokenType.QString && token.Type != ImapTokenType.Atom) { - Debug.WriteLine ("Expected string token as first element in namespace pair, but got: {0}", token); - throw UnexpectedToken (GenericUntaggedResponseSyntaxErrorFormat, "NAMESPACE", token); - } + AssertToken (token, ImapTokenType.Atom, ImapTokenType.QString, GenericUntaggedResponseSyntaxErrorFormat, "NAMESPACE", token); path = (string) token.Value; // second token is the directory separator - token = Stream.ReadToken (cancellationToken); + token = ReadToken (cancellationToken); - if (token.Type != ImapTokenType.QString && token.Type != ImapTokenType.Nil) { - Debug.WriteLine ("Expected string or nil token as second element in namespace pair, but got: {0}", token); - throw UnexpectedToken (GenericUntaggedResponseSyntaxErrorFormat, "NAMESPACE", token); - } + AssertToken (token, ImapTokenType.QString, ImapTokenType.Nil, GenericUntaggedResponseSyntaxErrorFormat, "NAMESPACE", token); var qstring = token.Type == ImapTokenType.Nil ? string.Empty : (string) token.Value; @@ -1023,7 +1609,7 @@ void UpdateNamespaces (CancellationToken cancellationToken) namespaces[n].Add (new FolderNamespace (delim, DecodeMailboxName (path))); - if (!GetCachedFolder (path, out folder)) { + if (!TryGetCachedFolder (path, out var folder)) { folder = CreateImapFolder (path, FolderAttributes.None, delim); CacheFolder (folder); } @@ -1031,219 +1617,394 @@ void UpdateNamespaces (CancellationToken cancellationToken) folder.UpdateIsNamespace (true); do { - token = Stream.ReadToken (cancellationToken); + token = ReadToken (cancellationToken); if (token.Type == ImapTokenType.CloseParen) break; // NAMESPACE extension - if (token.Type != ImapTokenType.QString && token.Type != ImapTokenType.Atom) - throw UnexpectedToken (GenericUntaggedResponseSyntaxErrorFormat, "NAMESPACE", token); + AssertToken (token, ImapTokenType.Atom, ImapTokenType.QString, GenericUntaggedResponseSyntaxErrorFormat, "NAMESPACE", token); - token = Stream.ReadToken (cancellationToken); + token = ReadToken (cancellationToken); - if (token.Type != ImapTokenType.OpenParen) - throw UnexpectedToken (GenericUntaggedResponseSyntaxErrorFormat, "NAMESPACE", token); + AssertToken (token, ImapTokenType.OpenParen, GenericUntaggedResponseSyntaxErrorFormat, "NAMESPACE", token); do { - token = Stream.ReadToken (cancellationToken); + token = ReadToken (cancellationToken); if (token.Type == ImapTokenType.CloseParen) break; - if (token.Type != ImapTokenType.QString && token.Type != ImapTokenType.Atom) - throw UnexpectedToken (GenericUntaggedResponseSyntaxErrorFormat, "NAMESPACE", token); + AssertToken (token, ImapTokenType.Atom, ImapTokenType.QString, GenericUntaggedResponseSyntaxErrorFormat, "NAMESPACE", token); } while (true); } while (true); // read the next token - it should either be '(' or ')' - token = Stream.ReadToken (cancellationToken); + token = ReadToken (cancellationToken); } - if (token.Type != ImapTokenType.CloseParen) { - Debug.WriteLine ("Expected ')' to close namespace pair, but got: {0}", token); - throw UnexpectedToken (GenericUntaggedResponseSyntaxErrorFormat, "NAMESPACE", token); - } - } else if (token.Type != ImapTokenType.Nil) { - Debug.WriteLine ("Expected '(' or 'NIL' token after untagged 'NAMESPACE' response, but got: {0}", token); - throw UnexpectedToken (GenericUntaggedResponseSyntaxErrorFormat, "NAMESPACE", token); + AssertToken (token, ImapTokenType.CloseParen, GenericUntaggedResponseSyntaxErrorFormat, "NAMESPACE", token); + } else { + AssertToken (token, ImapTokenType.Nil, GenericUntaggedResponseSyntaxErrorFormat, "NAMESPACE", token); } - token = Stream.ReadToken (cancellationToken); + token = ReadToken (cancellationToken); n++; } while (n < 3); while (token.Type != ImapTokenType.Eoln) - token = Stream.ReadToken (cancellationToken); - } - - void ProcessResponseCodes (ImapCommand ic) - { - foreach (var code in ic.RespCodes) { - if (code.Type == ImapResponseCodeType.Alert) { - OnAlert (code.Message); - break; - } - } - } - - static ImapResponseCodeType GetResponseCodeType (string atom) - { - switch (atom) { - case "ALERT": return ImapResponseCodeType.Alert; - case "BADCHARSET": return ImapResponseCodeType.BadCharset; - case "CAPABILITY": return ImapResponseCodeType.Capability; - case "NEWNAME": return ImapResponseCodeType.NewName; - case "PARSE": return ImapResponseCodeType.Parse; - case "PERMANENTFLAGS": return ImapResponseCodeType.PermanentFlags; - case "READ-ONLY": return ImapResponseCodeType.ReadOnly; - case "READ-WRITE": return ImapResponseCodeType.ReadWrite; - case "TRYCREATE": return ImapResponseCodeType.TryCreate; - case "UIDNEXT": return ImapResponseCodeType.UidNext; - case "UIDVALIDITY": return ImapResponseCodeType.UidValidity; - case "UNSEEN": return ImapResponseCodeType.Unseen; - case "REFERRAL": return ImapResponseCodeType.Referral; - case "UNKNOWN-CTE": return ImapResponseCodeType.UnknownCte; - case "APPENDUID": return ImapResponseCodeType.AppendUid; - case "COPYUID": return ImapResponseCodeType.CopyUid; - case "UIDNOTSTICKY": return ImapResponseCodeType.UidNotSticky; - case "URLMECH": return ImapResponseCodeType.UrlMech; - case "BADURL": return ImapResponseCodeType.BadUrl; - case "TOOBIG": return ImapResponseCodeType.TooBig; - case "HIGHESTMODSEQ": return ImapResponseCodeType.HighestModSeq; - case "MODIFIED": return ImapResponseCodeType.Modified; - case "NOMODSEQ": return ImapResponseCodeType.NoModSeq; - case "COMPRESSIONACTIVE": return ImapResponseCodeType.CompressionActive; - case "CLOSED": return ImapResponseCodeType.Closed; - case "NOTSAVED": return ImapResponseCodeType.NotSaved; - case "BADCOMPARATOR": return ImapResponseCodeType.BadComparator; - case "ANNOTATE": return ImapResponseCodeType.Annotate; - case "ANNOTATIONS": return ImapResponseCodeType.Annotations; - case "MAXCONVERTMESSAGES": return ImapResponseCodeType.MaxConvertMessages; - case "MAXCONVERTPARTS": return ImapResponseCodeType.MaxConvertParts; - case "TEMPFAIL": return ImapResponseCodeType.TempFail; - case "NOUPDATE": return ImapResponseCodeType.NoUpdate; - case "METADATA": return ImapResponseCodeType.Metadata; - case "NOTIFICATIONOVERFLOW": return ImapResponseCodeType.NotificationOverflow; - case "BADEVENT": return ImapResponseCodeType.BadEvent; - case "UNDEFINED-FILTER": return ImapResponseCodeType.UndefinedFilter; - case "UNAVAILABLE": return ImapResponseCodeType.Unavailable; - case "AUTHENTICATIONFAILED": return ImapResponseCodeType.AuthenticationFailed; - case "AUTHORIZATIONFAILED": return ImapResponseCodeType.AuthorizationFailed; - case "EXPIRED": return ImapResponseCodeType.Expired; - case "PRIVACYREQUIRED": return ImapResponseCodeType.PrivacyRequired; - case "CONTACTADMIN": return ImapResponseCodeType.ContactAdmin; - case "NOPERM": return ImapResponseCodeType.NoPerm; - case "INUSE": return ImapResponseCodeType.InUse; - case "EXPUNGEISSUED": return ImapResponseCodeType.ExpungeIssued; - case "CORRUPTION": return ImapResponseCodeType.Corruption; - case "SERVERBUG": return ImapResponseCodeType.ServerBug; - case "CLIENTBUG": return ImapResponseCodeType.ClientBug; - case "CANNOT": return ImapResponseCodeType.CanNot; - case "LIMIT": return ImapResponseCodeType.Limit; - case "OVERQUOTA": return ImapResponseCodeType.OverQuota; - case "ALREADYEXISTS": return ImapResponseCodeType.AlreadyExists; - case "NONEXISTENT": return ImapResponseCodeType.NonExistent; - case "USEATTR": return ImapResponseCodeType.UseAttr; - default: return ImapResponseCodeType.Unknown; - } + token = ReadToken (cancellationToken); } - /// - /// Parses the response code. - /// - /// The response code. - /// The cancellation token. - public ImapResponseCode ParseResponseCode (CancellationToken cancellationToken) + async ValueTask UpdateNamespacesAsync (CancellationToken cancellationToken) { - uint validity = Selected != null ? Selected.UidValidity : 0; - ImapResponseCode code; + var namespaces = new List { + PersonalNamespaces, OtherNamespaces, SharedNamespaces + }; ImapToken token; - string atom; - ulong n64; - uint n32; + string path; + char delim; + int n = 0; -// token = Stream.ReadToken (cancellationToken); -// -// if (token.Type != ImapTokenType.LeftBracket) { -// Debug.WriteLine ("Expected a '[' followed by a RESP-CODE, but got: {0}", token); -// throw UnexpectedToken (token, false); -// } + PersonalNamespaces.Clear (); + SharedNamespaces.Clear (); + OtherNamespaces.Clear (); - token = Stream.ReadToken (cancellationToken); + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); - if (token.Type != ImapTokenType.Atom) { - Debug.WriteLine ("Expected an atom token containing a RESP-CODE, but got: {0}", token); - throw UnexpectedToken ("Syntax error in response code. Unexpected token: {0}", token); - } + do { + if (token.Type == ImapTokenType.OpenParen) { + // parse the list of namespace pairs... + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); - atom = (string) token.Value; - token = Stream.ReadToken (cancellationToken); + while (token.Type == ImapTokenType.OpenParen) { + // parse the namespace pair - first token is the path + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); - code = ImapResponseCode.Create (GetResponseCodeType (atom)); + AssertToken (token, ImapTokenType.Atom, ImapTokenType.QString, GenericUntaggedResponseSyntaxErrorFormat, "NAMESPACE", token); - switch (code.Type) { - case ImapResponseCodeType.BadCharset: - if (token.Type == ImapTokenType.OpenParen) { - token = Stream.ReadToken (cancellationToken); + path = (string) token.Value; - SupportedCharsets.Clear (); - while (token.Type == ImapTokenType.Atom || token.Type == ImapTokenType.QString) { - SupportedCharsets.Add ((string) token.Value); - token = Stream.ReadToken (cancellationToken); - } + // second token is the directory separator + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); - if (token.Type != ImapTokenType.CloseParen) { - Debug.WriteLine ("Expected ')' after list of charsets in 'BADCHARSET' RESP-CODE, but got: {0}", token); - throw UnexpectedToken (GenericResponseCodeSyntaxErrorFormat, "BADCHARSET", token); - } + AssertToken (token, ImapTokenType.QString, ImapTokenType.Nil, GenericUntaggedResponseSyntaxErrorFormat, "NAMESPACE", token); - token = Stream.ReadToken (cancellationToken); - } - break; - case ImapResponseCodeType.Capability: - Stream.UngetToken (token); - UpdateCapabilities (ImapTokenType.CloseBracket, cancellationToken); - token = ReadToken (cancellationToken); - break; - case ImapResponseCodeType.PermanentFlags: - var perm = (PermanentFlagsResponseCode) code; + var qstring = token.Type == ImapTokenType.Nil ? string.Empty : (string) token.Value; - Stream.UngetToken (token); - perm.Flags = ImapUtils.ParseFlagsList (this, "PERMANENTFLAGS", null, cancellationToken); - token = Stream.ReadToken (cancellationToken); - break; - case ImapResponseCodeType.UidNext: - var next = (UidNextResponseCode) code; + if (qstring.Length > 0) { + delim = qstring[0]; - if (token.Type != ImapTokenType.Atom || !uint.TryParse ((string) token.Value, out n32) || n32 == 0) { - Debug.WriteLine ("Expected nz-number argument to 'UIDNEXT' RESP-CODE, but got: {0}", token); - throw UnexpectedToken (GenericResponseCodeSyntaxErrorFormat, "UIDNEXT", token); - } + // canonicalize the namespace path + path = path.TrimEnd (delim); + } else { + delim = '\0'; + } - next.Uid = new UniqueId (n32); + namespaces[n].Add (new FolderNamespace (delim, DecodeMailboxName (path))); - token = Stream.ReadToken (cancellationToken); - break; - case ImapResponseCodeType.UidValidity: - var uidvalidity = (UidValidityResponseCode) code; + if (!TryGetCachedFolder (path, out var folder)) { + folder = CreateImapFolder (path, FolderAttributes.None, delim); + CacheFolder (folder); + } - // Note: we allow '0' here because some servers have been known to send "* OK [UIDVALIDITY 0]". - // The *probable* explanation here is that the folder has never been opened and/or no messages - // have ever been delivered (yet) to that mailbox and so the UNIDVALIDITY has not (yet) been - // initialized. - // - // See https://github.com/jstedfast/MailKit/issues/150 for an example. - if (token.Type != ImapTokenType.Atom || !uint.TryParse ((string) token.Value, out n32)) { - Debug.WriteLine ("Expected nz-number argument to 'UIDVALIDITY' RESP-CODE, but got: {0}", token); - throw UnexpectedToken (GenericResponseCodeSyntaxErrorFormat, "UIDVALIDITY", token); - } + folder.UpdateIsNamespace (true); - uidvalidity.UidValidity = n32; + do { + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); - token = Stream.ReadToken (cancellationToken); - break; + if (token.Type == ImapTokenType.CloseParen) + break; + + // NAMESPACE extension + + AssertToken (token, ImapTokenType.Atom, ImapTokenType.QString, GenericUntaggedResponseSyntaxErrorFormat, "NAMESPACE", token); + + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + AssertToken (token, ImapTokenType.OpenParen, GenericUntaggedResponseSyntaxErrorFormat, "NAMESPACE", token); + + do { + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.CloseParen) + break; + + AssertToken (token, ImapTokenType.Atom, ImapTokenType.QString, GenericUntaggedResponseSyntaxErrorFormat, "NAMESPACE", token); + } while (true); + } while (true); + + // read the next token - it should either be '(' or ')' + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + } + + AssertToken (token, ImapTokenType.CloseParen, GenericUntaggedResponseSyntaxErrorFormat, "NAMESPACE", token); + } else { + AssertToken (token, ImapTokenType.Nil, GenericUntaggedResponseSyntaxErrorFormat, "NAMESPACE", token); + } + + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + n++; + } while (n < 3); + + while (token.Type != ImapTokenType.Eoln) + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + } + + void ProcessResponseCodes (ImapCommand ic) + { + foreach (var code in ic.RespCodes) { + switch (code.Type) { + case ImapResponseCodeType.Alert: + OnAlert (code.Message); + break; + case ImapResponseCodeType.WebAlert: + var webAlert = (WebAlertResponseCode) code; + if (webAlert.WebUri != null) + OnWebAlert (webAlert.WebUri, code.Message); + break; + case ImapResponseCodeType.NotificationOverflow: + OnNotificationOverflow (); + break; + } + } + } + + void EmitMetadataChanged (Metadata metadata) + { + var encodedName = metadata.EncodedName; + + if (encodedName.Length == 0) { + OnMetadataChanged (metadata); + } else if (FolderCache.TryGetValue (encodedName, out var folder)) { + folder.OnMetadataChanged (metadata); + } + } + + internal MetadataCollection FilterMetadata (MetadataCollection metadata, string encodedName) + { + for (int i = 0; i < metadata.Count; i++) { + if (metadata[i].EncodedName == encodedName) + continue; + + EmitMetadataChanged (metadata[i]); + metadata.RemoveAt (i); + i--; + } + + return metadata; + } + + internal void ProcessMetadataChanges (MetadataCollection metadata) + { + for (int i = 0; i < metadata.Count; i++) + EmitMetadataChanged (metadata[i]); + } + + internal static ImapResponseCodeType GetResponseCodeType (string atom) + { + if (atom.Equals ("ALERT", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.Alert; + if (atom.Equals ("BADCHARSET", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.BadCharset; + if (atom.Equals ("CAPABILITY", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.Capability; + if (atom.Equals ("NEWNAME", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.NewName; + if (atom.Equals ("PARSE", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.Parse; + if (atom.Equals ("PERMANENTFLAGS", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.PermanentFlags; + if (atom.Equals ("READ-ONLY", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.ReadOnly; + if (atom.Equals ("READ-WRITE", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.ReadWrite; + if (atom.Equals ("TRYCREATE", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.TryCreate; + if (atom.Equals ("UIDNEXT", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.UidNext; + if (atom.Equals ("UIDVALIDITY", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.UidValidity; + if (atom.Equals ("UNSEEN", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.Unseen; + if (atom.Equals ("REFERRAL", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.Referral; + if (atom.Equals ("UNKNOWN-CTE", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.UnknownCte; + if (atom.Equals ("APPENDUID", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.AppendUid; + if (atom.Equals ("COPYUID", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.CopyUid; + if (atom.Equals ("UIDNOTSTICKY", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.UidNotSticky; + if (atom.Equals ("URLMECH", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.UrlMech; + if (atom.Equals ("BADURL", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.BadUrl; + if (atom.Equals ("TOOBIG", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.TooBig; + if (atom.Equals ("HIGHESTMODSEQ", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.HighestModSeq; + if (atom.Equals ("MODIFIED", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.Modified; + if (atom.Equals ("NOMODSEQ", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.NoModSeq; + if (atom.Equals ("COMPRESSIONACTIVE", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.CompressionActive; + if (atom.Equals ("CLOSED", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.Closed; + if (atom.Equals ("NOTSAVED", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.NotSaved; + if (atom.Equals ("BADCOMPARATOR", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.BadComparator; + if (atom.Equals ("ANNOTATE", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.Annotate; + if (atom.Equals ("ANNOTATIONS", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.Annotations; + if (atom.Equals ("MAXCONVERTMESSAGES", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.MaxConvertMessages; + if (atom.Equals ("MAXCONVERTPARTS", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.MaxConvertParts; + if (atom.Equals ("TEMPFAIL", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.TempFail; + if (atom.Equals ("NOUPDATE", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.NoUpdate; + if (atom.Equals ("METADATA", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.Metadata; + if (atom.Equals ("NOTIFICATIONOVERFLOW", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.NotificationOverflow; + if (atom.Equals ("BADEVENT", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.BadEvent; + if (atom.Equals ("UNDEFINED-FILTER", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.UndefinedFilter; + if (atom.Equals ("UNAVAILABLE", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.Unavailable; + if (atom.Equals ("AUTHENTICATIONFAILED", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.AuthenticationFailed; + if (atom.Equals ("AUTHORIZATIONFAILED", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.AuthorizationFailed; + if (atom.Equals ("EXPIRED", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.Expired; + if (atom.Equals ("PRIVACYREQUIRED", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.PrivacyRequired; + if (atom.Equals ("CONTACTADMIN", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.ContactAdmin; + if (atom.Equals ("NOPERM", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.NoPerm; + if (atom.Equals ("INUSE", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.InUse; + if (atom.Equals ("EXPUNGEISSUED", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.ExpungeIssued; + if (atom.Equals ("CORRUPTION", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.Corruption; + if (atom.Equals ("SERVERBUG", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.ServerBug; + if (atom.Equals ("CLIENTBUG", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.ClientBug; + if (atom.Equals ("CANNOT", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.CanNot; + if (atom.Equals ("LIMIT", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.Limit; + if (atom.Equals ("OVERQUOTA", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.OverQuota; + if (atom.Equals ("ALREADYEXISTS", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.AlreadyExists; + if (atom.Equals ("NONEXISTENT", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.NonExistent; + if (atom.Equals ("USEATTR", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.UseAttr; + if (atom.Equals ("MAILBOXID", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.MailboxId; + if (atom.Equals ("WEBALERT", StringComparison.OrdinalIgnoreCase)) + return ImapResponseCodeType.WebAlert; + + return ImapResponseCodeType.Unknown; + } + + /// + /// Parses the response code. + /// + /// The response code. + /// Whether or not the resp-code is tagged vs untagged. + /// The cancellation token. + public ImapResponseCode ParseResponseCode (bool isTagged, CancellationToken cancellationToken) + { + uint validity = Selected != null ? Selected.UidValidity : 0; + ImapResponseCode code; + string atom, value; + ImapToken token; + + // token = ReadToken (cancellationToken); + // + // if (token.Type != ImapTokenType.LeftBracket) { + // Debug.WriteLine ("Expected a '[' followed by a RESP-CODE, but got: {0}", token); + // throw UnexpectedToken (token, false); + // } + + token = ReadToken (cancellationToken); + + AssertToken (token, ImapTokenType.Atom, "Syntax error in response code. {0}", token); + + atom = (string) token.Value; + token = ReadToken (cancellationToken); + + code = ImapResponseCode.Create (GetResponseCodeType (atom)); + code.IsTagged = isTagged; + + switch (code.Type) { + case ImapResponseCodeType.BadCharset: + if (token.Type == ImapTokenType.OpenParen) { + token = ReadToken (cancellationToken); + + SupportedCharsets.Clear (); + while (token.Type == ImapTokenType.Atom || token.Type == ImapTokenType.QString) { + SupportedCharsets.Add ((string) token.Value); + token = ReadToken (cancellationToken); + } + + AssertToken (token, ImapTokenType.CloseParen, GenericResponseCodeSyntaxErrorFormat, "BADCHARSET", token); + + token = ReadToken (cancellationToken); + } + break; + case ImapResponseCodeType.Capability: + UngetToken (token); + UpdateCapabilities (ImapTokenType.CloseBracket, cancellationToken); + token = ReadToken (cancellationToken); + break; + case ImapResponseCodeType.PermanentFlags: + var perm = (PermanentFlagsResponseCode) code; + + UngetToken (token); + perm.Flags = ImapUtils.ParseFlagsList (this, "PERMANENTFLAGS", perm.Keywords, cancellationToken); + token = ReadToken (cancellationToken); + break; + case ImapResponseCodeType.UidNext: + var next = (UidNextResponseCode) code; + + // Note: we allow '0' here because some servers have been known to send "* OK [UIDNEXT 0]". + // The *probable* explanation here is that the folder has never been opened and/or no messages + // have ever been delivered (yet) to that mailbox and so the UIDNEXT has not (yet) been + // initialized. + // + // See https://github.com/jstedfast/MailKit/issues/1010 for an example. + var uid = ParseNumber (token, false, GenericResponseCodeSyntaxErrorFormat, "UIDNEXT", token); + next.Uid = uid > 0 ? new UniqueId (uid) : UniqueId.Invalid; + token = ReadToken (cancellationToken); + break; + case ImapResponseCodeType.UidValidity: + var uidvalidity = (UidValidityResponseCode) code; + + // Note: we allow '0' here because some servers have been known to send "* OK [UIDVALIDITY 0]". + // The *probable* explanation here is that the folder has never been opened and/or no messages + // have ever been delivered (yet) to that mailbox and so the UIDVALIDITY has not (yet) been + // initialized. + // + // See https://github.com/jstedfast/MailKit/issues/150 for an example. + uidvalidity.UidValidity = ParseNumber (token, false, GenericResponseCodeSyntaxErrorFormat, "UIDVALIDITY", token); + token = ReadToken (cancellationToken); + break; case ImapResponseCodeType.Unseen: var unseen = (UnseenResponseCode) code; @@ -1251,14 +2012,11 @@ public ImapResponseCode ParseResponseCode (CancellationToken cancellationToken) // mailbox contains no messages. // // See https://github.com/jstedfast/MailKit/issues/34 for details. - if (token.Type != ImapTokenType.Atom || !uint.TryParse ((string) token.Value, out n32)) { - Debug.WriteLine ("Expected nz-number argument to 'UNSEEN' RESP-CODE, but got: {0}", token); - throw UnexpectedToken (GenericResponseCodeSyntaxErrorFormat, "UNSEEN", token); - } + var n = ParseNumber (token, false, GenericResponseCodeSyntaxErrorFormat, "UNSEEN", token); - unseen.Index = n32 > 0 ? (int) (n32 - 1) : 0; + unseen.Index = n > 0 ? (int) (n - 1) : 0; - token = Stream.ReadToken (cancellationToken); + token = ReadToken (cancellationToken); break; case ImapResponseCodeType.NewName: var rename = (NewNameResponseCode) code; @@ -1268,524 +2026,1203 @@ public ImapResponseCode ParseResponseCode (CancellationToken cancellationToken) // 85) Remove NEWNAME. It can't work because mailbox names can be // literals and can include "]". Functionality can be addressed via // referrals. - if (token.Type != ImapTokenType.Atom && token.Type != ImapTokenType.QString) { - Debug.WriteLine ("Expected atom or qstring as first argument to 'NEWNAME' RESP-CODE, but got: {0}", token); - throw UnexpectedToken (GenericResponseCodeSyntaxErrorFormat, "NEWNAME", token); - } + AssertToken (token, ImapTokenType.Atom, ImapTokenType.QString, GenericResponseCodeSyntaxErrorFormat, "NEWNAME", token); rename.OldName = (string) token.Value; // the next token should be another atom or qstring token representing the new name of the folder - token = Stream.ReadToken (cancellationToken); + token = ReadToken (cancellationToken); - if (token.Type != ImapTokenType.Atom && token.Type != ImapTokenType.QString) { - Debug.WriteLine ("Expected atom or qstring as second argument to 'NEWNAME' RESP-CODE, but got: {0}", token); - throw UnexpectedToken (GenericResponseCodeSyntaxErrorFormat, "NEWNAME", token); - } + AssertToken (token, ImapTokenType.Atom, ImapTokenType.QString, GenericResponseCodeSyntaxErrorFormat, "NEWNAME", token); rename.NewName = (string) token.Value; - token = Stream.ReadToken (cancellationToken); + token = ReadToken (cancellationToken); break; case ImapResponseCodeType.AppendUid: var append = (AppendUidResponseCode) code; - if (token.Type != ImapTokenType.Atom || !uint.TryParse ((string) token.Value, out n32)) { - Debug.WriteLine ("Expected nz-number as first argument of the 'APPENDUID' RESP-CODE, but got: {0}", token); - throw UnexpectedToken (GenericResponseCodeSyntaxErrorFormat, "APPENDUID", token); - } - - append.UidValidity = n32; + append.UidValidity = ParseNumber (token, false, GenericResponseCodeSyntaxErrorFormat, "APPENDUID", token); - token = Stream.ReadToken (cancellationToken); + token = ReadToken (cancellationToken); // The MULTIAPPEND extension redefines APPENDUID's second argument to be a uid-set instead of a single uid. - if (token.Type != ImapTokenType.Atom || !UniqueIdSet.TryParse ((string) token.Value, n32, out append.UidSet)) { - Debug.WriteLine ("Expected nz-number or uid-set as second argument to 'APPENDUID' RESP-CODE, but got: {0}", token); - throw UnexpectedToken (GenericResponseCodeSyntaxErrorFormat, "APPENDUID", token); - } + append.UidSet = ParseUidSet (token, append.UidValidity, out _, out _, GenericResponseCodeSyntaxErrorFormat, "APPENDUID", token); - token = Stream.ReadToken (cancellationToken); + token = ReadToken (cancellationToken); break; case ImapResponseCodeType.CopyUid: var copy = (CopyUidResponseCode) code; - if (token.Type != ImapTokenType.Atom || !uint.TryParse ((string) token.Value, out n32)) { - Debug.WriteLine ("Expected nz-number as first argument of the 'COPYUID' RESP-CODE, but got: {0}", token); - throw UnexpectedToken (GenericResponseCodeSyntaxErrorFormat, "COPYUID", token); - } + copy.UidValidity = ParseNumber (token, false, GenericResponseCodeSyntaxErrorFormat, "COPYUID", token); - copy.UidValidity = n32; + token = ReadToken (cancellationToken); - token = Stream.ReadToken (cancellationToken); + // Note: Outlook.com will apparently sometimes issue a [COPYUID nz_number SPACE SPACE] resp-code + // in response to a UID COPY or UID MOVE command. Likely this happens only when the source message + // didn't exist or something? See https://github.com/jstedfast/MailKit/issues/555 for details. - if (token.Type != ImapTokenType.Atom || !UniqueIdSet.TryParse ((string) token.Value, validity, out copy.SrcUidSet)) { - Debug.WriteLine ("Expected uid-set as second argument to 'COPYUID' RESP-CODE, but got: {0}", token); - throw UnexpectedToken (GenericResponseCodeSyntaxErrorFormat, "COPYUID", token); + if (token.Type != ImapTokenType.CloseBracket) { + copy.SrcUidSet = ParseUidSet (token, validity, out _, out _, GenericResponseCodeSyntaxErrorFormat, "COPYUID", token); + } else { + copy.SrcUidSet = new UniqueIdSet (); + UngetToken (token); } - token = Stream.ReadToken (cancellationToken); + token = ReadToken (cancellationToken); - if (token.Type != ImapTokenType.Atom || !UniqueIdSet.TryParse ((string) token.Value, n32, out copy.DestUidSet)) { - Debug.WriteLine ("Expected uid-set as third argument to 'COPYUID' RESP-CODE, but got: {0}", token); - throw UnexpectedToken (GenericResponseCodeSyntaxErrorFormat, "COPYUID", token); + if (token.Type != ImapTokenType.CloseBracket) { + copy.DestUidSet = ParseUidSet (token, copy.UidValidity, out _, out _, GenericResponseCodeSyntaxErrorFormat, "COPYUID", token); + } else { + copy.DestUidSet = new UniqueIdSet (); + UngetToken (token); } - token = Stream.ReadToken (cancellationToken); + token = ReadToken (cancellationToken); break; case ImapResponseCodeType.BadUrl: var badurl = (BadUrlResponseCode) code; - if (token.Type != ImapTokenType.Atom && token.Type != ImapTokenType.QString) { - Debug.WriteLine ("Expected url-resp-text as argument to the 'BADURL' RESP-CODE, but got: {0}", token); - throw UnexpectedToken (GenericResponseCodeSyntaxErrorFormat, "BADURL", token); - } + AssertToken (token, ImapTokenType.Atom, ImapTokenType.QString, GenericResponseCodeSyntaxErrorFormat, "BADURL", token); badurl.BadUrl = (string) token.Value; - token = Stream.ReadToken (cancellationToken); + token = ReadToken (cancellationToken); break; case ImapResponseCodeType.HighestModSeq: var highest = (HighestModSeqResponseCode) code; - if (token.Type != ImapTokenType.Atom || !ulong.TryParse ((string) token.Value, out n64)) { - Debug.WriteLine ("Expected 64-bit nz-number as first argument of the 'HIGHESTMODSEQ' RESP-CODE, but got: {0}", token); - throw UnexpectedToken (GenericResponseCodeSyntaxErrorFormat, "HIGHESTMODSEQ", token); - } - - highest.HighestModSeq = n64; + highest.HighestModSeq = ParseNumber64 (token, false, GenericResponseCodeSyntaxErrorFormat, "HIGHESTMODSEQ", token); - token = Stream.ReadToken (cancellationToken); + token = ReadToken (cancellationToken); break; case ImapResponseCodeType.Modified: var modified = (ModifiedResponseCode) code; - if (token.Type != ImapTokenType.Atom || !UniqueIdSet.TryParse ((string) token.Value, validity, out modified.UidSet)) { - Debug.WriteLine ("Expected uid-set argument to 'MODIFIED' RESP-CODE, but got: {0}", token); - throw UnexpectedToken (GenericResponseCodeSyntaxErrorFormat, "MODIFIED", token); - } + modified.UidSet = ParseUidSet (token, validity, out _, out _, GenericResponseCodeSyntaxErrorFormat, "MODIFIED", token); - token = Stream.ReadToken (cancellationToken); + token = ReadToken (cancellationToken); break; case ImapResponseCodeType.MaxConvertMessages: case ImapResponseCodeType.MaxConvertParts: var maxConvert = (MaxConvertResponseCode) code; - if (token.Type != ImapTokenType.Atom || !int.TryParse ((string) token.Value, out maxConvert.MaxConvert)) { - Debug.WriteLine ("Expected number argument to '{0}' RESP-CODE, but got: {1}", code.Type.ToString ().ToUpperInvariant (), token); - throw UnexpectedToken (GenericResponseCodeSyntaxErrorFormat, code.Type.ToString ().ToUpperInvariant (), token); - } + maxConvert.MaxConvert = ParseNumber (token, false, GenericResponseCodeSyntaxErrorFormat, atom, token); - token = Stream.ReadToken (cancellationToken); + token = ReadToken (cancellationToken); break; case ImapResponseCodeType.NoUpdate: var noUpdate = (NoUpdateResponseCode) code; - if (token.Type != ImapTokenType.Atom && token.Type != ImapTokenType.QString) { - Debug.WriteLine ("Expected string argument to 'NOUPDATE' RESP-CODE, but got: {0}", token); - throw UnexpectedToken (GenericResponseCodeSyntaxErrorFormat, "NOUPDATE", token); - } + AssertToken (token, ImapTokenType.Atom, ImapTokenType.QString, GenericResponseCodeSyntaxErrorFormat, "NOUPDATE", token); noUpdate.Tag = (string) token.Value; - token = Stream.ReadToken (cancellationToken); + token = ReadToken (cancellationToken); break; - case ImapResponseCodeType.Metadata: - var metadata = (MetadataResponseCode) code; + case ImapResponseCodeType.Annotate: + var annotate = (AnnotateResponseCode) code; - if (token.Type != ImapTokenType.Atom) { - Debug.WriteLine ("Expected atom argument to 'METADATA' RESP-CODE, but got: {0}", token); - throw UnexpectedToken (GenericResponseCodeSyntaxErrorFormat, "METADATA", token); - } + AssertToken (token, ImapTokenType.Atom, GenericResponseCodeSyntaxErrorFormat, "ANNOTATE", token); - switch ((string) token.Value) { - case "LONGENTRIES": - metadata.SubType = MetadataResponseCodeSubType.LongEntries; + value = (string) token.Value; + if (value.Equals ("TOOBIG", StringComparison.OrdinalIgnoreCase)) + annotate.SubType = AnnotateResponseCodeSubType.TooBig; + else if (value.Equals ("TOOMANY", StringComparison.OrdinalIgnoreCase)) + annotate.SubType = AnnotateResponseCodeSubType.TooMany; - token = Stream.ReadToken (cancellationToken); + token = ReadToken (cancellationToken); + break; + case ImapResponseCodeType.Annotations: + var annotations = (AnnotationsResponseCode) code; - if (token.Type != ImapTokenType.Atom || !uint.TryParse ((string) token.Value, out n32)) { - Debug.WriteLine ("Expected integer argument to 'METADATA LONGENTRIES' RESP-CODE, but got: {0}", token); - throw UnexpectedToken (GenericResponseCodeSyntaxErrorFormat, "METADATA LONGENTRIES", token); - } + AssertToken (token, ImapTokenType.Atom, GenericResponseCodeSyntaxErrorFormat, "ANNOTATIONS", token); - metadata.Value = n32; - break; - case "MAXSIZE": - metadata.SubType = MetadataResponseCodeSubType.MaxSize; + value = (string) token.Value; + if (value.Equals ("NONE", StringComparison.OrdinalIgnoreCase)) { + // nothing + } else if (value.Equals ("READ-ONLY", StringComparison.OrdinalIgnoreCase)) { + annotations.Access = AnnotationAccess.ReadOnly; + } else { + annotations.Access = AnnotationAccess.ReadWrite; + annotations.MaxSize = ParseNumber (token, false, GenericResponseCodeSyntaxErrorFormat, "ANNOTATIONS", token); + } - token = Stream.ReadToken (cancellationToken); + token = ReadToken (cancellationToken); - if (token.Type != ImapTokenType.Atom || !uint.TryParse ((string) token.Value, out n32)) { - Debug.WriteLine ("Expected integer argument to 'METADATA MAXSIZE' RESP-CODE, but got: {0}", token); - throw UnexpectedToken (GenericResponseCodeSyntaxErrorFormat, "METADATA MAXSIZE", token); - } + if (annotations.Access != AnnotationAccess.None) { + annotations.Scopes = AnnotationScope.Both; - metadata.Value = n32; - break; - case "TOOMANY": - metadata.SubType = MetadataResponseCodeSubType.TooMany; - break; - case "NOPRIVATE": - metadata.SubType = MetadataResponseCodeSubType.NoPrivate; - break; - } + if (token.Type != ImapTokenType.CloseBracket) { + AssertToken (token, ImapTokenType.Atom, GenericResponseCodeSyntaxErrorFormat, "ANNOTATIONS", token); - token = Stream.ReadToken (cancellationToken); - break; - case ImapResponseCodeType.UndefinedFilter: - var undefined = (UndefinedFilterResponseCode) code; + if (((string) token.Value).Equals ("NOPRIVATE", StringComparison.OrdinalIgnoreCase)) + annotations.Scopes = AnnotationScope.Shared; - if (token.Type != ImapTokenType.Atom) { - Debug.WriteLine ("Expected atom argument to 'UNDEFINED-FILTER' RESP-CODE, but got: {0}", token); - throw UnexpectedToken (GenericResponseCodeSyntaxErrorFormat, "UNDEFINED-FILTER", token); + token = ReadToken (cancellationToken); + } } - undefined.Name = (string) token.Value; - - token = Stream.ReadToken (cancellationToken); break; - default: - if (code.Type == ImapResponseCodeType.Unknown) - Debug.WriteLine (string.Format ("Unknown RESP-CODE encountered: {0}", atom)); - - // extensions are of the form: "[" atom [SPACE 1*] "]" + case ImapResponseCodeType.Metadata: + var metadata = (MetadataResponseCode) code; - // skip over tokens until we get to a ']' - while (token.Type != ImapTokenType.CloseBracket && token.Type != ImapTokenType.Eoln) - token = Stream.ReadToken (cancellationToken); + AssertToken (token, ImapTokenType.Atom, GenericResponseCodeSyntaxErrorFormat, "METADATA", token); - break; - } + value = (string) token.Value; + if (value.Equals ("LONGENTRIES", StringComparison.OrdinalIgnoreCase)) { + metadata.SubType = MetadataResponseCodeSubType.LongEntries; + metadata.IsError = false; - if (token.Type != ImapTokenType.CloseBracket) { - Debug.WriteLine ("Expected ']' after '{0}' RESP-CODE, but got: {1}", atom, token); - throw UnexpectedToken ("Syntax error in response code. Unexpected token: {0}", token); - } + token = ReadToken (cancellationToken); - code.Message = ReadLine (cancellationToken).Trim (); + metadata.Value = ParseNumber (token, false, GenericResponseCodeSyntaxErrorFormat, "METADATA LONGENTRIES", token); + } else if (value.Equals ("MAXSIZE", StringComparison.OrdinalIgnoreCase)) { + metadata.SubType = MetadataResponseCodeSubType.MaxSize; - return code; - } + token = ReadToken (cancellationToken); - void UpdateStatus (CancellationToken cancellationToken) - { - var token = Stream.ReadToken (cancellationToken); - ImapFolder folder; - uint uid, limit; - ulong modseq; - string name; - int count; + metadata.Value = ParseNumber (token, false, GenericResponseCodeSyntaxErrorFormat, "METADATA MAXSIZE", token); + } else if (value.Equals ("TOOMANY", StringComparison.OrdinalIgnoreCase)) { + metadata.SubType = MetadataResponseCodeSubType.TooMany; + } else if (value.Equals ("NOPRIVATE", StringComparison.OrdinalIgnoreCase)) { + metadata.SubType = MetadataResponseCodeSubType.NoPrivate; + } - switch (token.Type) { - case ImapTokenType.Literal: - name = ReadLiteral (cancellationToken); - break; - case ImapTokenType.QString: - case ImapTokenType.Atom: - name = (string) token.Value; - break; - case ImapTokenType.Nil: - // Note: according to rfc3501, section 4.5, NIL is acceptable as a mailbox name. - name = "NIL"; + token = ReadToken (cancellationToken); break; - default: - throw UnexpectedToken (GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); - } - - // Note: if the folder is null, then it probably means the user is using NOTIFY - // and hasn't yet requested the folder. That's ok. - GetCachedFolder (name, out folder); - - token = Stream.ReadToken (cancellationToken); - - if (token.Type != ImapTokenType.OpenParen) - throw UnexpectedToken (GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); + case ImapResponseCodeType.UndefinedFilter: + var undefined = (UndefinedFilterResponseCode) code; - do { - token = Stream.ReadToken (cancellationToken); + AssertToken (token, ImapTokenType.Atom, GenericResponseCodeSyntaxErrorFormat, "UNDEFINED-FILTER", token); - if (token.Type == ImapTokenType.CloseParen) - break; + undefined.Name = (string) token.Value; - if (token.Type != ImapTokenType.Atom) - throw UnexpectedToken (GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); + token = ReadToken (cancellationToken); + break; + case ImapResponseCodeType.MailboxId: + var mailboxid = (MailboxIdResponseCode) code; - var atom = (string) token.Value; + AssertToken (token, ImapTokenType.OpenParen, GenericResponseCodeSyntaxErrorFormat, "MAILBOXID", token); - token = Stream.ReadToken (cancellationToken); + token = ReadToken (cancellationToken); - switch (atom) { - case "HIGHESTMODSEQ": - if (token.Type != ImapTokenType.Atom) - throw UnexpectedToken (GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); + AssertToken (token, ImapTokenType.Atom, GenericResponseCodeSyntaxErrorFormat, "MAILBOXID", token); - if (!ulong.TryParse ((string) token.Value, out modseq)) - throw UnexpectedToken (GenericItemSyntaxErrorFormat, atom, token); + mailboxid.MailboxId = (string) token.Value; - if (folder != null) - folder.UpdateHighestModSeq (modseq); - break; - case "MESSAGES": - if (token.Type != ImapTokenType.Atom) - throw UnexpectedToken (GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); + token = ReadToken (cancellationToken); - if (!int.TryParse ((string) token.Value, out count)) - throw UnexpectedToken (GenericItemSyntaxErrorFormat, atom, token); + AssertToken (token, ImapTokenType.CloseParen, GenericResponseCodeSyntaxErrorFormat, "MAILBOXID", token); - if (folder != null) - folder.OnExists (count); - break; - case "RECENT": - if (token.Type != ImapTokenType.Atom) - throw UnexpectedToken (GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); + token = ReadToken (cancellationToken); + break; + case ImapResponseCodeType.WebAlert: + var webalert = (WebAlertResponseCode) code; - if (!int.TryParse ((string) token.Value, out count)) - throw UnexpectedToken (GenericItemSyntaxErrorFormat, atom, token); + AssertToken (token, ImapTokenType.Atom, GenericResponseCodeSyntaxErrorFormat, "WEBALERT", token); - if (folder != null) - folder.OnRecent (count); - break; - case "UIDNEXT": - if (token.Type != ImapTokenType.Atom) - throw UnexpectedToken (GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); + Uri.TryCreate ((string) token.Value, UriKind.Absolute, out webalert.WebUri); - if (!uint.TryParse ((string) token.Value, out uid)) - throw UnexpectedToken (GenericItemSyntaxErrorFormat, atom, token); + token = ReadToken (cancellationToken); + break; + default: + // Note: This code-path handles: [ALERT], [CLOSED], [READ-ONLY], [READ-WRITE], etc. - if (folder != null) - folder.UpdateUidNext (uid > 0 ? new UniqueId (uid) : UniqueId.Invalid); - break; - case "UIDVALIDITY": - if (token.Type != ImapTokenType.Atom) - throw UnexpectedToken (GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); + //if (code.Type == ImapResponseCodeType.Unknown) + // Debug.WriteLine (string.Format ("Unknown RESP-CODE encountered: {0}", atom)); - if (!uint.TryParse ((string) token.Value, out uid)) - throw UnexpectedToken (GenericItemSyntaxErrorFormat, atom, token); + // extensions are of the form: "[" atom [SPACE 1*] "]" - if (folder != null) - folder.UpdateUidValidity (uid); - break; - case "UNSEEN": - if (token.Type != ImapTokenType.Atom) - throw UnexpectedToken (GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); + // skip over tokens until we get to a ']' + while (token.Type != ImapTokenType.CloseBracket && token.Type != ImapTokenType.Eoln) + token = ReadToken (cancellationToken); - if (!int.TryParse ((string) token.Value, out count)) - throw UnexpectedToken (GenericItemSyntaxErrorFormat, atom, token); + break; + } - if (folder != null) - folder.UpdateUnread (count); - break; - case "APPENDLIMIT": - if (token.Type == ImapTokenType.Atom) { - if (!uint.TryParse ((string) token.Value, out limit)) - throw UnexpectedToken (GenericItemSyntaxErrorFormat, atom, token); - - if (folder != null) - folder.UpdateAppendLimit (limit); - } else if (token.Type == ImapTokenType.Nil) { - if (folder != null) - folder.UpdateAppendLimit (null); - } else { - throw UnexpectedToken (GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); - } - break; - } - } while (true); + AssertToken (token, ImapTokenType.CloseBracket, "Syntax error in response code. {0}", token); - token = Stream.ReadToken (cancellationToken); + code.Message = ReadLine (cancellationToken).Trim (); - if (token.Type != ImapTokenType.Eoln) - throw UnexpectedToken (GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); + return code; } /// - /// Processes an untagged response. + /// Parses the response code. /// - /// The untagged response. + /// The response code. + /// Whether or not the resp-code is tagged vs untagged. /// The cancellation token. - internal ImapUntaggedResult ProcessUntaggedResponse (CancellationToken cancellationToken) + public async ValueTask ParseResponseCodeAsync (bool isTagged, CancellationToken cancellationToken) { - var result = ImapUntaggedResult.Handled; - var token = Stream.ReadToken (cancellationToken); - ImapUntaggedHandler handler; - ImapFolder folder; - uint number; + uint validity = Selected != null ? Selected.UidValidity : 0; + ImapResponseCode code; + string atom, value; + ImapToken token; - if (current != null && current.Folder != null) - folder = current.Folder; - else - folder = Selected; + // token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + // + // if (token.Type != ImapTokenType.LeftBracket) { + // Debug.WriteLine ("Expected a '[' followed by a RESP-CODE, but got: {0}", token); + // throw UnexpectedToken (token, false); + // } - if (token.Type == ImapTokenType.Atom) { - var atom = (string) token.Value; + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); - switch (atom) { - case "BYE": - token = Stream.ReadToken (cancellationToken); + AssertToken (token, ImapTokenType.Atom, "Syntax error in response code. {0}", token); - if (token.Type == ImapTokenType.OpenBracket) { - var code = ParseResponseCode (cancellationToken); - if (current != null) - current.RespCodes.Add (code); - } + atom = (string) token.Value; + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); - ReadLine (cancellationToken); + code = ImapResponseCode.Create (GetResponseCodeType (atom)); + code.IsTagged = isTagged; - if (current != null) { - current.Bye = true; - } else { - Disconnect (); + switch (code.Type) { + case ImapResponseCodeType.BadCharset: + if (token.Type == ImapTokenType.OpenParen) { + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + SupportedCharsets.Clear (); + while (token.Type == ImapTokenType.Atom || token.Type == ImapTokenType.QString) { + SupportedCharsets.Add ((string) token.Value); + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); } - break; - case "CAPABILITY": - UpdateCapabilities (ImapTokenType.Eoln, cancellationToken); - // read the eoln token - Stream.ReadToken (cancellationToken); - break; - case "FLAGS": - folder.UpdateAcceptedFlags (ImapUtils.ParseFlagsList (this, atom, null, cancellationToken)); - token = Stream.ReadToken (cancellationToken); + AssertToken (token, ImapTokenType.CloseParen, GenericResponseCodeSyntaxErrorFormat, "BADCHARSET", token); + + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + } + break; + case ImapResponseCodeType.Capability: + UngetToken (token); + await UpdateCapabilitiesAsync (ImapTokenType.CloseBracket, cancellationToken).ConfigureAwait (false); + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + break; + case ImapResponseCodeType.PermanentFlags: + var perm = (PermanentFlagsResponseCode) code; + + UngetToken (token); + perm.Flags = await ImapUtils.ParseFlagsListAsync (this, "PERMANENTFLAGS", perm.Keywords, cancellationToken).ConfigureAwait (false); + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + break; + case ImapResponseCodeType.UidNext: + var next = (UidNextResponseCode) code; + + // Note: we allow '0' here because some servers have been known to send "* OK [UIDNEXT 0]". + // The *probable* explanation here is that the folder has never been opened and/or no messages + // have ever been delivered (yet) to that mailbox and so the UIDNEXT has not (yet) been + // initialized. + // + // See https://github.com/jstedfast/MailKit/issues/1010 for an example. + var uid = ParseNumber (token, false, GenericResponseCodeSyntaxErrorFormat, "UIDNEXT", token); + next.Uid = uid > 0 ? new UniqueId (uid) : UniqueId.Invalid; + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + break; + case ImapResponseCodeType.UidValidity: + var uidvalidity = (UidValidityResponseCode) code; + + // Note: we allow '0' here because some servers have been known to send "* OK [UIDVALIDITY 0]". + // The *probable* explanation here is that the folder has never been opened and/or no messages + // have ever been delivered (yet) to that mailbox and so the UIDVALIDITY has not (yet) been + // initialized. + // + // See https://github.com/jstedfast/MailKit/issues/150 for an example. + uidvalidity.UidValidity = ParseNumber (token, false, GenericResponseCodeSyntaxErrorFormat, "UIDVALIDITY", token); + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + break; + case ImapResponseCodeType.Unseen: + var unseen = (UnseenResponseCode) code; + + // Note: we allow '0' here because some servers have been known to send "* OK [UNSEEN 0]" when the + // mailbox contains no messages. + // + // See https://github.com/jstedfast/MailKit/issues/34 for details. + var n = ParseNumber (token, false, GenericResponseCodeSyntaxErrorFormat, "UNSEEN", token); + + unseen.Index = n > 0 ? (int) (n - 1) : 0; + + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + break; + case ImapResponseCodeType.NewName: + var rename = (NewNameResponseCode) code; + + // Note: this RESP-CODE existed in rfc2060 but has been removed in rfc3501: + // + // 85) Remove NEWNAME. It can't work because mailbox names can be + // literals and can include "]". Functionality can be addressed via + // referrals. + AssertToken (token, ImapTokenType.Atom, ImapTokenType.QString, GenericResponseCodeSyntaxErrorFormat, "NEWNAME", token); + + rename.OldName = (string) token.Value; + + // the next token should be another atom or qstring token representing the new name of the folder + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + AssertToken (token, ImapTokenType.Atom, ImapTokenType.QString, GenericResponseCodeSyntaxErrorFormat, "NEWNAME", token); + + rename.NewName = (string) token.Value; + + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + break; + case ImapResponseCodeType.AppendUid: + var append = (AppendUidResponseCode) code; + + append.UidValidity = ParseNumber (token, false, GenericResponseCodeSyntaxErrorFormat, "APPENDUID", token); + + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + // The MULTIAPPEND extension redefines APPENDUID's second argument to be a uid-set instead of a single uid. + append.UidSet = ParseUidSet (token, append.UidValidity, out _, out _, GenericResponseCodeSyntaxErrorFormat, "APPENDUID", token); + + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + break; + case ImapResponseCodeType.CopyUid: + var copy = (CopyUidResponseCode) code; + + copy.UidValidity = ParseNumber (token, false, GenericResponseCodeSyntaxErrorFormat, "COPYUID", token); + + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + // Note: Outlook.com will apparently sometimes issue a [COPYUID nz_number SPACE SPACE] resp-code + // in response to a UID COPY or UID MOVE command. Likely this happens only when the source message + // didn't exist or something? See https://github.com/jstedfast/MailKit/issues/555 for details. + + if (token.Type != ImapTokenType.CloseBracket) { + copy.SrcUidSet = ParseUidSet (token, validity, out _, out _, GenericResponseCodeSyntaxErrorFormat, "COPYUID", token); + } else { + copy.SrcUidSet = new UniqueIdSet (); + UngetToken (token); + } + + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + if (token.Type != ImapTokenType.CloseBracket) { + copy.DestUidSet = ParseUidSet (token, copy.UidValidity, out _, out _, GenericResponseCodeSyntaxErrorFormat, "COPYUID", token); + } else { + copy.DestUidSet = new UniqueIdSet (); + UngetToken (token); + } + + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + break; + case ImapResponseCodeType.BadUrl: + var badurl = (BadUrlResponseCode) code; + + AssertToken (token, ImapTokenType.Atom, ImapTokenType.QString, GenericResponseCodeSyntaxErrorFormat, "BADURL", token); + + badurl.BadUrl = (string) token.Value; + + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + break; + case ImapResponseCodeType.HighestModSeq: + var highest = (HighestModSeqResponseCode) code; + + highest.HighestModSeq = ParseNumber64 (token, false, GenericResponseCodeSyntaxErrorFormat, "HIGHESTMODSEQ", token); + + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + break; + case ImapResponseCodeType.Modified: + var modified = (ModifiedResponseCode) code; + + modified.UidSet = ParseUidSet (token, validity, out _, out _, GenericResponseCodeSyntaxErrorFormat, "MODIFIED", token); + + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + break; + case ImapResponseCodeType.MaxConvertMessages: + case ImapResponseCodeType.MaxConvertParts: + var maxConvert = (MaxConvertResponseCode) code; + + maxConvert.MaxConvert = ParseNumber (token, false, GenericResponseCodeSyntaxErrorFormat, atom, token); + + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + break; + case ImapResponseCodeType.NoUpdate: + var noUpdate = (NoUpdateResponseCode) code; - if (token.Type != ImapTokenType.Eoln) { - Debug.WriteLine ("Expected eoln after untagged FLAGS list, but got: {0}", token); - throw UnexpectedToken (GenericUntaggedResponseSyntaxErrorFormat, atom, token); + AssertToken (token, ImapTokenType.Atom, ImapTokenType.QString, GenericResponseCodeSyntaxErrorFormat, "NOUPDATE", token); + + noUpdate.Tag = (string) token.Value; + + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + break; + case ImapResponseCodeType.Annotate: + var annotate = (AnnotateResponseCode) code; + + AssertToken (token, ImapTokenType.Atom, GenericResponseCodeSyntaxErrorFormat, "ANNOTATE", token); + + value = (string) token.Value; + if (value.Equals ("TOOBIG", StringComparison.OrdinalIgnoreCase)) + annotate.SubType = AnnotateResponseCodeSubType.TooBig; + else if (value.Equals ("TOOMANY", StringComparison.OrdinalIgnoreCase)) + annotate.SubType = AnnotateResponseCodeSubType.TooMany; + + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + break; + case ImapResponseCodeType.Annotations: + var annotations = (AnnotationsResponseCode) code; + + AssertToken (token, ImapTokenType.Atom, GenericResponseCodeSyntaxErrorFormat, "ANNOTATIONS", token); + + value = (string) token.Value; + if (value.Equals ("NONE", StringComparison.OrdinalIgnoreCase)) { + // nothing + } else if (value.Equals ("READ-ONLY", StringComparison.OrdinalIgnoreCase)) { + annotations.Access = AnnotationAccess.ReadOnly; + } else { + annotations.Access = AnnotationAccess.ReadWrite; + annotations.MaxSize = ParseNumber (token, false, GenericResponseCodeSyntaxErrorFormat, "ANNOTATIONS", token); + } + + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + if (annotations.Access != AnnotationAccess.None) { + annotations.Scopes = AnnotationScope.Both; + + if (token.Type != ImapTokenType.CloseBracket) { + AssertToken (token, ImapTokenType.Atom, GenericResponseCodeSyntaxErrorFormat, "ANNOTATIONS", token); + + if (((string) token.Value).Equals ("NOPRIVATE", StringComparison.OrdinalIgnoreCase)) + annotations.Scopes = AnnotationScope.Shared; + + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); } + } + + break; + case ImapResponseCodeType.Metadata: + var metadata = (MetadataResponseCode) code; + + AssertToken (token, ImapTokenType.Atom, GenericResponseCodeSyntaxErrorFormat, "METADATA", token); + + value = (string) token.Value; + if (value.Equals ("LONGENTRIES", StringComparison.OrdinalIgnoreCase)) { + metadata.SubType = MetadataResponseCodeSubType.LongEntries; + metadata.IsError = false; + + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + metadata.Value = ParseNumber (token, false, GenericResponseCodeSyntaxErrorFormat, "METADATA LONGENTRIES", token); + } else if (value.Equals ("MAXSIZE", StringComparison.OrdinalIgnoreCase)) { + metadata.SubType = MetadataResponseCodeSubType.MaxSize; + + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + metadata.Value = ParseNumber (token, false, GenericResponseCodeSyntaxErrorFormat, "METADATA MAXSIZE", token); + } else if (value.Equals ("TOOMANY", StringComparison.OrdinalIgnoreCase)) { + metadata.SubType = MetadataResponseCodeSubType.TooMany; + } else if (value.Equals ("NOPRIVATE", StringComparison.OrdinalIgnoreCase)) { + metadata.SubType = MetadataResponseCodeSubType.NoPrivate; + } + + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + break; + case ImapResponseCodeType.UndefinedFilter: + var undefined = (UndefinedFilterResponseCode) code; + + AssertToken (token, ImapTokenType.Atom, GenericResponseCodeSyntaxErrorFormat, "UNDEFINED-FILTER", token); + + undefined.Name = (string) token.Value; + + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + break; + case ImapResponseCodeType.MailboxId: + var mailboxid = (MailboxIdResponseCode) code; + + AssertToken (token, ImapTokenType.OpenParen, GenericResponseCodeSyntaxErrorFormat, "MAILBOXID", token); + + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + AssertToken (token, ImapTokenType.Atom, GenericResponseCodeSyntaxErrorFormat, "MAILBOXID", token); + + mailboxid.MailboxId = (string) token.Value; + + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + AssertToken (token, ImapTokenType.CloseParen, GenericResponseCodeSyntaxErrorFormat, "MAILBOXID", token); + + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + break; + case ImapResponseCodeType.WebAlert: + var webalert = (WebAlertResponseCode) code; + + AssertToken (token, ImapTokenType.Atom, GenericResponseCodeSyntaxErrorFormat, "WEBALERT", token); + + Uri.TryCreate ((string) token.Value, UriKind.Absolute, out webalert.WebUri); + + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + break; + default: + // Note: This code-path handles: [ALERT], [CLOSED], [READ-ONLY], [READ-WRITE], etc. + + //if (code.Type == ImapResponseCodeType.Unknown) + // Debug.WriteLine (string.Format ("Unknown RESP-CODE encountered: {0}", atom)); + + // extensions are of the form: "[" atom [SPACE 1*] "]" + + // skip over tokens until we get to a ']' + while (token.Type != ImapTokenType.CloseBracket && token.Type != ImapTokenType.Eoln) + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + break; + } + + AssertToken (token, ImapTokenType.CloseBracket, "Syntax error in response code. {0}", token); + + code.Message = (await ReadLineAsync (cancellationToken).ConfigureAwait (false)).Trim (); + + return code; + } + + static bool UpdateSimpleStatusValue (ImapFolder? folder, string atom, ImapToken token) + { + uint count, uid; + ulong modseq; + + if (atom.Equals ("HIGHESTMODSEQ", StringComparison.OrdinalIgnoreCase)) { + AssertToken (token, ImapTokenType.Atom, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); + + modseq = ParseNumber64 (token, false, GenericItemSyntaxErrorFormat, atom, token); + + folder?.UpdateHighestModSeq (modseq); + } else if (atom.Equals ("MESSAGES", StringComparison.OrdinalIgnoreCase)) { + AssertToken (token, ImapTokenType.Atom, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); + + count = ParseNumber (token, false, GenericItemSyntaxErrorFormat, atom, token); + + folder?.OnExists ((int) count); + } else if (atom.Equals ("RECENT", StringComparison.OrdinalIgnoreCase)) { + AssertToken (token, ImapTokenType.Atom, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); + + count = ParseNumber (token, false, GenericItemSyntaxErrorFormat, atom, token); + + folder?.OnRecent ((int) count); + } else if (atom.Equals ("UIDNEXT", StringComparison.OrdinalIgnoreCase)) { + AssertToken (token, ImapTokenType.Atom, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); + + uid = ParseNumber (token, false, GenericItemSyntaxErrorFormat, atom, token); + + folder?.UpdateUidNext (uid > 0 ? new UniqueId (uid) : UniqueId.Invalid); + } else if (atom.Equals ("UIDVALIDITY", StringComparison.OrdinalIgnoreCase)) { + AssertToken (token, ImapTokenType.Atom, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); + + uid = ParseNumber (token, false, GenericItemSyntaxErrorFormat, atom, token); + + folder?.UpdateUidValidity (uid); + } else if (atom.Equals ("UNSEEN", StringComparison.OrdinalIgnoreCase)) { + AssertToken (token, ImapTokenType.Atom, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); + + count = ParseNumber (token, false, GenericItemSyntaxErrorFormat, atom, token); + + folder?.UpdateUnread ((int) count); + } else if (atom.Equals ("APPENDLIMIT", StringComparison.OrdinalIgnoreCase)) { + if (token.Type == ImapTokenType.Atom) { + var limit = ParseNumber (token, false, GenericItemSyntaxErrorFormat, atom, token); + + folder?.UpdateAppendLimit (limit); + } else { + AssertToken (token, ImapTokenType.Nil, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); + + folder?.UpdateAppendLimit (null); + } + } else if (atom.Equals ("SIZE", StringComparison.OrdinalIgnoreCase)) { + AssertToken (token, ImapTokenType.Atom, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); + + var size = ParseNumber64 (token, false, GenericItemSyntaxErrorFormat, atom, token); + + folder?.UpdateSize (size); + } else { + // This is probably the MAILBOXID value which is multiple tokens and can't be handled here. + return false; + } + + return true; + } + + void UpdateStatus (CancellationToken cancellationToken) + { + var token = ReadToken (ImapStream.AtomSpecials, cancellationToken); + string name; + + switch (token.Type) { + case ImapTokenType.Literal: + name = ReadLiteral (cancellationToken); + break; + case ImapTokenType.QString: + case ImapTokenType.Atom: + name = (string) token.Value; + break; + case ImapTokenType.Nil: + // Note: according to rfc3501, section 4.5, NIL is acceptable as a mailbox name. + name = (string) token.Value; + break; + default: + throw UnexpectedToken (GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); + } + + // Note: if the folder is null, then it probably means the user is using NOTIFY + // and hasn't yet requested the folder. That's ok. + TryGetCachedFolder (name, out var folder); + + token = ReadToken (cancellationToken); + + AssertToken (token, ImapTokenType.OpenParen, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); + + do { + token = ReadToken (cancellationToken); + + if (token.Type == ImapTokenType.CloseParen) break; - case "NAMESPACE": - UpdateNamespaces (cancellationToken); - break; - case "STATUS": - UpdateStatus (cancellationToken); - break; - case "OK": case "NO": case "BAD": - if (atom == "OK") - result = ImapUntaggedResult.Ok; - else if (atom == "NO") - result = ImapUntaggedResult.No; - else - result = ImapUntaggedResult.Bad; - - token = Stream.ReadToken (cancellationToken); - - if (token.Type == ImapTokenType.OpenBracket) { - var code = ParseResponseCode (cancellationToken); - if (current != null) - current.RespCodes.Add (code); - } else if (token.Type != ImapTokenType.Eoln) { - var text = ((string) token.Value) + ReadLine (cancellationToken); - - if (current != null) - current.ResponseText = text.TrimEnd (); - } + + AssertToken (token, ImapTokenType.Atom, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); + + var atom = (string) token.Value; + + token = ReadToken (cancellationToken); + + if (UpdateSimpleStatusValue (folder, atom, token)) + continue; + + if (atom.Equals ("MAILBOXID", StringComparison.OrdinalIgnoreCase)) { + AssertToken (token, ImapTokenType.OpenParen, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); + + token = ReadToken (cancellationToken); + + AssertToken (token, ImapTokenType.Atom, GenericItemSyntaxErrorFormat, atom, token); + + folder?.UpdateId ((string) token.Value); + + token = ReadToken (cancellationToken); + + AssertToken (token, ImapTokenType.CloseParen, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); + } + } while (true); + + token = ReadToken (cancellationToken); + + AssertToken (token, ImapTokenType.Eoln, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); + } + + async ValueTask UpdateStatusAsync (CancellationToken cancellationToken) + { + var token = await ReadTokenAsync (ImapStream.AtomSpecials, cancellationToken).ConfigureAwait (false); + string name; + + switch (token.Type) { + case ImapTokenType.Literal: + name = await ReadLiteralAsync (cancellationToken).ConfigureAwait (false); + break; + case ImapTokenType.QString: + case ImapTokenType.Atom: + name = (string) token.Value; + break; + case ImapTokenType.Nil: + // Note: according to rfc3501, section 4.5, NIL is acceptable as a mailbox name. + name = (string) token.Value; + break; + default: + throw UnexpectedToken (GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); + } + + // Note: if the folder is null, then it probably means the user is using NOTIFY + // and hasn't yet requested the folder. That's ok. + TryGetCachedFolder (name, out var folder); + + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + AssertToken (token, ImapTokenType.OpenParen, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); + + do { + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.CloseParen) break; - default: - if (uint.TryParse (atom, out number)) { - // we probably have something like "* 1 EXISTS" - token = Stream.ReadToken (cancellationToken); - - if (token.Type != ImapTokenType.Atom) { - // protocol error - Debug.WriteLine ("Unhandled untagged response: * {0} {1}", number, atom); - throw UnexpectedToken ("Syntax error in untagged response. Unexpected token: {0}", token); - } - atom = (string) token.Value; + AssertToken (token, ImapTokenType.Atom, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); - if (current != null && current.UntaggedHandlers.TryGetValue (atom, out handler)) { - // the command registered an untagged handler for this atom... - handler (this, current, (int) number - 1); - } else if (folder != null) { - switch (atom) { - case "EXISTS": - folder.OnExists ((int) number); - break; - case "EXPUNGE": - if (number == 0) - throw UnexpectedToken ("Syntax error in untagged EXPUNGE response. Unexpected message index: 0"); + var atom = (string) token.Value; - folder.OnExpunge ((int) number - 1); - break; - case "FETCH": - // Apparently Courier-IMAP (2004) will reply with "* 0 FETCH ..." sometimes. - // See https://github.com/jstedfast/MailKit/issues/428 for details. - //if (number == 0) - // throw UnexpectedToken ("Syntax error in untagged FETCH response. Unexpected message index: 0"); + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); - folder.OnFetch (this, (int) number - 1, cancellationToken); - break; - case "RECENT": - folder.OnRecent ((int) number); - break; - default: - Debug.WriteLine ("Unhandled untagged response: * {0} {1}", number, atom); - break; - } + if (UpdateSimpleStatusValue (folder, atom, token)) + continue; + + if (atom.Equals ("MAILBOXID", StringComparison.OrdinalIgnoreCase)) { + AssertToken (token, ImapTokenType.OpenParen, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); + + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + AssertToken (token, ImapTokenType.Atom, GenericItemSyntaxErrorFormat, atom, token); + + folder?.UpdateId ((string) token.Value); + + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + AssertToken (token, ImapTokenType.CloseParen, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); + } + } while (true); + + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + AssertToken (token, ImapTokenType.Eoln, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); + } + + static bool IsOkNoOrBad (string atom, out ImapUntaggedResult result) + { + if (atom.Equals ("OK", StringComparison.OrdinalIgnoreCase)) { + result = ImapUntaggedResult.Ok; + return true; + } + + if (atom.Equals ("NO", StringComparison.OrdinalIgnoreCase)) { + result = ImapUntaggedResult.No; + return true; + } + + if (atom.Equals ("BAD", StringComparison.OrdinalIgnoreCase)) { + result = ImapUntaggedResult.Bad; + return true; + } + + result = ImapUntaggedResult.Ok; + + return false; + } + + /// + /// Processes an untagged response. + /// + /// The untagged response. + /// The IMAP command that is currently being processed. + /// The cancellation token. + internal void ProcessUntaggedResponse (ImapCommand ic, CancellationToken cancellationToken) + { + var token = ReadToken (cancellationToken); + var folder = ic.Folder ?? Selected; + ImapUntaggedHandler? handler; + string atom; + + // Note: work around broken IMAP servers such as home.pl which sends "* [COPYUID ...]" resp-codes + // See https://github.com/jstedfast/MailKit/issues/115#issuecomment-313684616 for details. + if (token.Type == ImapTokenType.OpenBracket) { + // unget the '[' token and then pretend that we got an "OK" + UngetToken (token); + atom = "OK"; + } else if (token.Type != ImapTokenType.Atom) { + // if we get anything else here, just ignore it? + UngetToken (token); + SkipLine (cancellationToken); + return; + } else { + atom = (string) token.Value; + } + + if (atom.Equals ("BYE", StringComparison.OrdinalIgnoreCase)) { + token = ReadToken (cancellationToken); + + if (token.Type == ImapTokenType.OpenBracket) { + var code = ParseResponseCode (false, cancellationToken); + ic.RespCodes.Add (code); + } else { + var text = ReadLine (cancellationToken).TrimEnd (); + ic.ResponseText = token.Value.ToString () + text; + } + + ic.Bye = true; + + // Note: Yandex IMAP is broken and will continue sending untagged BYE responses until the client closes + // the connection. In order to avoid this scenario, consider this command complete as soon as we receive + // the very first untagged BYE response and do not hold out hoping for a tagged response following the + // untagged BYE. + // + // See https://github.com/jstedfast/MailKit/issues/938 for details. + if (QuirksMode == ImapQuirksMode.Yandex && !ic.Logout) + ic.Status = ImapCommandStatus.Complete; + } else if (atom.Equals ("CAPABILITY", StringComparison.OrdinalIgnoreCase)) { + UpdateCapabilities (ImapTokenType.Eoln, cancellationToken); + + // read the eoln token + ReadToken (cancellationToken); + } else if (atom.Equals ("ENABLED", StringComparison.OrdinalIgnoreCase)) { + do { + token = ReadToken (cancellationToken); + + if (token.Type == ImapTokenType.Eoln) + break; + + AssertToken (token, ImapTokenType.Atom, GenericUntaggedResponseSyntaxErrorFormat, atom, token); + + var feature = (string) token.Value; + if (feature.Equals ("UTF8=ACCEPT", StringComparison.OrdinalIgnoreCase)) + UTF8Enabled = true; + else if (feature.Equals ("QRESYNC", StringComparison.OrdinalIgnoreCase)) + QResyncEnabled = true; + } while (true); + } else if (atom.Equals ("FLAGS", StringComparison.OrdinalIgnoreCase)) { + var keywords = new HashSet (StringComparer.Ordinal); + var flags = ImapUtils.ParseFlagsList (this, atom, keywords, cancellationToken); + folder?.UpdateAcceptedFlags (flags, keywords); + token = ReadToken (cancellationToken); + + AssertToken (token, ImapTokenType.Eoln, GenericUntaggedResponseSyntaxErrorFormat, atom, token); + } else if (atom.Equals ("NAMESPACE", StringComparison.OrdinalIgnoreCase)) { + UpdateNamespaces (cancellationToken); + } else if (atom.Equals ("STATUS", StringComparison.OrdinalIgnoreCase)) { + UpdateStatus (cancellationToken); + } else if (IsOkNoOrBad (atom, out var result)) { + token = ReadToken (cancellationToken); + + if (token.Type == ImapTokenType.OpenBracket) { + var code = ParseResponseCode (false, cancellationToken); + ic.RespCodes.Add (code); + } else if (token.Type != ImapTokenType.Eoln) { + var text = ReadLine (cancellationToken).TrimEnd (); + ic.ResponseText = token.Value.ToString () + text; + } + } else { + if (uint.TryParse (atom, NumberStyles.None, CultureInfo.InvariantCulture, out uint number)) { + // we probably have something like "* 1 EXISTS" + token = ReadToken (cancellationToken); + + AssertToken (token, ImapTokenType.Atom, "Syntax error in untagged response. {0}", token); + + atom = (string) token.Value; + + if (ic.UntaggedHandlers.TryGetValue (atom, out handler)) { + // the command registered an untagged handler for this atom... + handler (this, ic, (int) number - 1, false).GetAwaiter ().GetResult (); + } else if (folder != null) { + if (atom.Equals ("EXISTS", StringComparison.OrdinalIgnoreCase)) { + folder.OnExists ((int) number); + } else if (atom.Equals ("EXPUNGE", StringComparison.OrdinalIgnoreCase)) { + if (number == 0) + throw UnexpectedToken ("Syntax error in untagged EXPUNGE response. Unexpected message index: 0"); + + folder.OnExpunge ((int) number - 1); + } else if (atom.Equals ("FETCH", StringComparison.OrdinalIgnoreCase)) { + // Apparently Courier-IMAP (2004) will reply with "* 0 FETCH ..." sometimes. + // See https://github.com/jstedfast/MailKit/issues/428 for details. + //if (number == 0) + // throw UnexpectedToken ("Syntax error in untagged FETCH response. Unexpected message index: 0"); + + folder.OnUntaggedFetchResponse (this, (int) number - 1, cancellationToken); + } else if (atom.Equals ("RECENT", StringComparison.OrdinalIgnoreCase)) { + folder.OnRecent ((int) number); } else { - Debug.WriteLine ("Unhandled untagged response: * {0} {1}", number, atom); + //Debug.WriteLine ("Unhandled untagged response: * {0} {1}", number, atom); } + } else { + //Debug.WriteLine ("Unhandled untagged response: * {0} {1}", number, atom); + } + + SkipLine (cancellationToken); + } else if (ic.UntaggedHandlers.TryGetValue (atom, out handler)) { + // the command registered an untagged handler for this atom... + handler (this, ic, -1, false).GetAwaiter ().GetResult (); + SkipLine (cancellationToken); + } else if (atom.Equals ("LIST", StringComparison.OrdinalIgnoreCase)) { + // unsolicited LIST response - probably due to NOTIFY MailboxName or MailboxSubscribe event + ImapUtils.ParseFolderList (this, null, false, true, cancellationToken); + token = ReadToken (cancellationToken); + AssertToken (token, ImapTokenType.Eoln, "Syntax error in untagged LIST response. {0}", token); + } else if (atom.Equals ("METADATA", StringComparison.OrdinalIgnoreCase)) { + // unsolicited METADATA response - probably due to NOTIFY MailboxMetadataChange or ServerMetadataChange + var metadata = new MetadataCollection (); + ImapUtils.ParseMetadata (this, metadata, cancellationToken); + ProcessMetadataChanges (metadata); + + token = ReadToken (cancellationToken); + AssertToken (token, ImapTokenType.Eoln, "Syntax error in untagged LIST response. {0}", token); + } else if (atom.Equals ("VANISHED", StringComparison.OrdinalIgnoreCase) && folder != null) { + folder.OnVanished (this, cancellationToken); + SkipLine (cancellationToken); + } else { + // don't know how to handle this... eat it? + SkipLine (cancellationToken); + } + } + } - SkipLine (cancellationToken); - } else if (current != null && current.UntaggedHandlers.TryGetValue (atom, out handler)) { + /// + /// Processes an untagged response. + /// + /// The untagged response. + /// The IMAP command that is currently being processed. + /// The cancellation token. + internal async Task ProcessUntaggedResponseAsync (ImapCommand ic, CancellationToken cancellationToken) + { + var token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + var folder = ic.Folder ?? Selected; + ImapUntaggedHandler? handler; + string atom; + + // Note: work around broken IMAP servers such as home.pl which sends "* [COPYUID ...]" resp-codes + // See https://github.com/jstedfast/MailKit/issues/115#issuecomment-313684616 for details. + if (token.Type == ImapTokenType.OpenBracket) { + // unget the '[' token and then pretend that we got an "OK" + UngetToken (token); + atom = "OK"; + } else if (token.Type != ImapTokenType.Atom) { + // if we get anything else here, just ignore it? + UngetToken (token); + await SkipLineAsync (cancellationToken).ConfigureAwait (false); + return; + } else { + atom = (string) token.Value; + } + + if (atom.Equals ("BYE", StringComparison.OrdinalIgnoreCase)) { + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.OpenBracket) { + var code = await ParseResponseCodeAsync (false, cancellationToken).ConfigureAwait (false); + ic.RespCodes.Add (code); + } else { + var text = (await ReadLineAsync (cancellationToken).ConfigureAwait (false)).TrimEnd (); + ic.ResponseText = token.Value.ToString () + text; + } + + ic.Bye = true; + + // Note: Yandex IMAP is broken and will continue sending untagged BYE responses until the client closes + // the connection. In order to avoid this scenario, consider this command complete as soon as we receive + // the very first untagged BYE response and do not hold out hoping for a tagged response following the + // untagged BYE. + // + // See https://github.com/jstedfast/MailKit/issues/938 for details. + if (QuirksMode == ImapQuirksMode.Yandex && !ic.Logout) + ic.Status = ImapCommandStatus.Complete; + } else if (atom.Equals ("CAPABILITY", StringComparison.OrdinalIgnoreCase)) { + await UpdateCapabilitiesAsync (ImapTokenType.Eoln, cancellationToken).ConfigureAwait (false); + + // read the eoln token + await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + } else if (atom.Equals ("ENABLED", StringComparison.OrdinalIgnoreCase)) { + do { + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.Eoln) + break; + + AssertToken (token, ImapTokenType.Atom, GenericUntaggedResponseSyntaxErrorFormat, atom, token); + + var feature = (string) token.Value; + if (feature.Equals ("UTF8=ACCEPT", StringComparison.OrdinalIgnoreCase)) + UTF8Enabled = true; + else if (feature.Equals ("QRESYNC", StringComparison.OrdinalIgnoreCase)) + QResyncEnabled = true; + } while (true); + } else if (atom.Equals ("FLAGS", StringComparison.OrdinalIgnoreCase)) { + var keywords = new HashSet (StringComparer.Ordinal); + var flags = await ImapUtils.ParseFlagsListAsync (this, atom, keywords, cancellationToken).ConfigureAwait (false); + folder?.UpdateAcceptedFlags (flags, keywords); + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + AssertToken (token, ImapTokenType.Eoln, GenericUntaggedResponseSyntaxErrorFormat, atom, token); + } else if (atom.Equals ("NAMESPACE", StringComparison.OrdinalIgnoreCase)) { + await UpdateNamespacesAsync (cancellationToken).ConfigureAwait (false); + } else if (atom.Equals ("STATUS", StringComparison.OrdinalIgnoreCase)) { + await UpdateStatusAsync (cancellationToken).ConfigureAwait (false); + } else if (IsOkNoOrBad (atom, out var result)) { + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.OpenBracket) { + var code = await ParseResponseCodeAsync (false, cancellationToken).ConfigureAwait (false); + ic.RespCodes.Add (code); + } else if (token.Type != ImapTokenType.Eoln) { + var text = (await ReadLineAsync (cancellationToken).ConfigureAwait (false)).TrimEnd (); + ic.ResponseText = token.Value.ToString () + text; + } + } else { + if (uint.TryParse (atom, NumberStyles.None, CultureInfo.InvariantCulture, out uint number)) { + // we probably have something like "* 1 EXISTS" + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + AssertToken (token, ImapTokenType.Atom, "Syntax error in untagged response. {0}", token); + + atom = (string) token.Value; + + if (ic.UntaggedHandlers.TryGetValue (atom, out handler)) { // the command registered an untagged handler for this atom... - handler (this, current, -1); - SkipLine (cancellationToken); - } else if (atom == "VANISHED" && folder != null) { - folder.OnVanished (this, cancellationToken); - SkipLine (cancellationToken); + await handler (this, ic, (int) number - 1, doAsync: true).ConfigureAwait (false); + } else if (folder != null) { + if (atom.Equals ("EXISTS", StringComparison.OrdinalIgnoreCase)) { + folder.OnExists ((int) number); + } else if (atom.Equals ("EXPUNGE", StringComparison.OrdinalIgnoreCase)) { + if (number == 0) + throw UnexpectedToken ("Syntax error in untagged EXPUNGE response. Unexpected message index: 0"); + + folder.OnExpunge ((int) number - 1); + } else if (atom.Equals ("FETCH", StringComparison.OrdinalIgnoreCase)) { + // Apparently Courier-IMAP (2004) will reply with "* 0 FETCH ..." sometimes. + // See https://github.com/jstedfast/MailKit/issues/428 for details. + //if (number == 0) + // throw UnexpectedToken ("Syntax error in untagged FETCH response. Unexpected message index: 0"); + + await folder.OnUntaggedFetchResponseAsync (this, (int) number - 1, cancellationToken).ConfigureAwait (false); + } else if (atom.Equals ("RECENT", StringComparison.OrdinalIgnoreCase)) { + folder.OnRecent ((int) number); + } else { + //Debug.WriteLine ("Unhandled untagged response: * {0} {1}", number, atom); + } } else { - // don't know how to handle this... eat it? - SkipLine (cancellationToken); + //Debug.WriteLine ("Unhandled untagged response: * {0} {1}", number, atom); } - break; + + await SkipLineAsync (cancellationToken).ConfigureAwait (false); + } else if (ic.UntaggedHandlers.TryGetValue (atom, out handler)) { + // the command registered an untagged handler for this atom... + await handler (this, ic, -1, doAsync: true).ConfigureAwait (false); + await SkipLineAsync (cancellationToken).ConfigureAwait (false); + } else if (atom.Equals ("LIST", StringComparison.OrdinalIgnoreCase)) { + // unsolicited LIST response - probably due to NOTIFY MailboxName or MailboxSubscribe event + await ImapUtils.ParseFolderListAsync (this, null, false, true, cancellationToken).ConfigureAwait (false); + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + AssertToken (token, ImapTokenType.Eoln, "Syntax error in untagged LIST response. {0}", token); + } else if (atom.Equals ("METADATA", StringComparison.OrdinalIgnoreCase)) { + // unsolicited METADATA response - probably due to NOTIFY MailboxMetadataChange or ServerMetadataChange + var metadata = new MetadataCollection (); + await ImapUtils.ParseMetadataAsync (this, metadata, cancellationToken).ConfigureAwait (false); + ProcessMetadataChanges (metadata); + + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + AssertToken (token, ImapTokenType.Eoln, "Syntax error in untagged LIST response. {0}", token); + } else if (atom.Equals ("VANISHED", StringComparison.OrdinalIgnoreCase) && folder != null) { + await folder.OnVanishedAsync (this, cancellationToken).ConfigureAwait (false); + await SkipLineAsync (cancellationToken).ConfigureAwait (false); + } else { + // don't know how to handle this... eat it? + await SkipLineAsync (cancellationToken).ConfigureAwait (false); } } + } + + [MemberNotNull (nameof (current))] + void PopNextCommand () + { + lock (queue) { + if (queue.Count == 0) + throw new InvalidOperationException ("The IMAP command queue is empty."); + + if (IsBusy) + throw new InvalidOperationException ("The ImapClient is currently busy processing a command in another thread. Lock the SyncRoot property to properly synchronize your threads."); - return result; + current = queue[0]; + queue.RemoveAt (0); + + try { + current.CancellationToken.ThrowIfCancellationRequested (); + } catch { + queue.RemoveAll (x => x.CancellationToken.IsCancellationRequested); + current = null; + throw; + } + } } /// - /// Iterate the command pipeline. + /// Handles an IMAP protocol exception by disconnecting and then potentially throwing a replacement exception. /// - public int Iterate () + /// The current being processed. + /// THe that was thrown. + /// + /// An ALERT or some resp-text was found that would enhance the exception message + /// of the provided. + /// + void OnImapProtocolException (ImapCommand ic, ImapProtocolException ex) { - if (Stream == null) - throw new InvalidOperationException (); + Disconnect (ex); + + if (ic.Bye) { + if (ic.RespCodes.Count > 0) { + var code = ic.RespCodes[ic.RespCodes.Count - 1]; + + if (code.Type == ImapResponseCodeType.Alert) { + OnAlert (code.Message); + + throw new ImapProtocolException (code.Message, ex); + } + } - if (queue.Count == 0) - throw new InvalidOperationException ("The IMAP command queue is empty."); + if (!string.IsNullOrEmpty (ic.ResponseText)) + throw new ImapProtocolException (ic.ResponseText!, ex); + } + } + + /// + /// Iterate the command pipeline. + /// + void Iterate () + { + PopNextCommand (); - current = queue[0]; - queue.RemoveAt (0); + current.Status = ImapCommandStatus.Active; try { - current.CancellationToken.ThrowIfCancellationRequested (); - } catch { - queue.RemoveAll (x => x.CancellationToken.IsCancellationRequested); - current = null; + while (current.Step ()) { + // more literal data to send... + } + + if (current.Bye && !current.Logout) + throw new ImapProtocolException ("Bye."); + } catch (ImapProtocolException ex) { + OnImapProtocolException (current, ex); throw; + } catch (Exception ex) { + Disconnect (ex); + throw; + } finally { + current = null; } + } - current.Status = ImapCommandStatus.Active; + /// + /// Asynchronously iterate the command pipeline. + /// + async Task IterateAsync () + { + PopNextCommand (); - int id = current.Id; + current.Status = ImapCommandStatus.Active; try { - while (current.Step ()) { + while (await current.StepAsync ().ConfigureAwait (false)) { // more literal data to send... } - if (current.Bye) - Disconnect (); - } catch { - Disconnect (); - throw; - } finally { - current = null; + if (current.Bye && !current.Logout) + throw new ImapProtocolException ("Bye."); + } catch (ImapProtocolException ex) { + OnImapProtocolException (current, ex); + throw; + } catch (Exception ex) { + Disconnect (ex); + throw; + } finally { + current = null; + } + } + + /// + /// Wait for the specified command to finish. + /// + /// The IMAP command. + /// + /// is . + /// + public ImapCommandResponse Run (ImapCommand ic) + { + if (ic == null) + throw new ArgumentNullException (nameof (ic)); + + while (ic.Status < ImapCommandStatus.Complete) { + // continue processing commands... + Iterate (); } - return id; + ProcessResponseCodes (ic); + + return ic.Response; } /// @@ -1793,15 +3230,78 @@ public int Iterate () /// /// The IMAP command. /// - /// is null. + /// is . /// - public void Wait (ImapCommand ic) + public async Task RunAsync (ImapCommand ic) { if (ic == null) throw new ArgumentNullException (nameof (ic)); - while (Iterate () < ic.Id) { + while (ic.Status < ImapCommandStatus.Complete) { // continue processing commands... + await IterateAsync ().ConfigureAwait (false); + } + + ProcessResponseCodes (ic); + + return ic.Response; + } + + public IEnumerable CreateCommands (CancellationToken cancellationToken, ImapFolder folder, string format, IList uids, params object[] args) + { + var vargs = new List (); + int maxLength; + + // we assume that uids is the first formatter (with a %s) + vargs.Add ("1"); + + for (int i = 0; i < args.Length; i++) + vargs.Add (args[i]); + + args = vargs.ToArray (); + + if (QuirksMode == ImapQuirksMode.Courier) { + // Courier IMAP's command parser allows each token to be up to 16k in size. + maxLength = 16 * 1024; + } else { + int estimated = ImapCommand.EstimateCommandLength (this, format, args); + + switch (QuirksMode) { + case ImapQuirksMode.Dovecot: + // Dovecot, by default, allows commands up to 64k. + // See https://github.com/dovecot/core/blob/master/src/imap/imap-settings.c#L94 + maxLength = Math.Max ((64 * 1042) - estimated, 24); + break; + case ImapQuirksMode.GMail: + // GMail seems to support command-lines up to at least 16k. + maxLength = Math.Max ((16 * 1042) - estimated, 24); + break; + case ImapQuirksMode.Yahoo: + case ImapQuirksMode.UW: + // Follow the IMAP4 Implementation Recommendations which states that clients + // *SHOULD* limit their command lengths to 1000 octets. + maxLength = Math.Max (1000 - estimated, 24); + break; + default: + // Push the boundaries of the IMAP4 Implementation Recommendations which states + // that servers *SHOULD* accept command lengths of up to 8000 octets. + maxLength = Math.Max (8000 - estimated, 24); + break; + } + } + + foreach (var subset in UniqueIdSet.EnumerateSerializedSubsets (uids, maxLength)) { + args[0] = subset; + + yield return new ImapCommand (this, cancellationToken, folder, format, args); + } + } + + public IEnumerable QueueCommands (CancellationToken cancellationToken, ImapFolder folder, string format, IList uids, params object[] args) + { + foreach (var ic in CreateCommands (cancellationToken, folder, format, uids, args)) { + QueueCommand (ic); + yield return ic; } } @@ -1814,7 +3314,7 @@ public void Wait (ImapCommand ic) /// The formatting options. /// The command format. /// The command arguments. - public ImapCommand QueueCommand (CancellationToken cancellationToken, ImapFolder folder, FormatOptions options, string format, params object[] args) + public ImapCommand QueueCommand (CancellationToken cancellationToken, ImapFolder? folder, FormatOptions options, string format, params object[] args) { var ic = new ImapCommand (this, cancellationToken, folder, options, format, args); QueueCommand (ic); @@ -1829,7 +3329,7 @@ public ImapCommand QueueCommand (CancellationToken cancellationToken, ImapFolder /// The folder that the command operates on. /// The command format. /// The command arguments. - public ImapCommand QueueCommand (CancellationToken cancellationToken, ImapFolder folder, string format, params object[] args) + public ImapCommand QueueCommand (CancellationToken cancellationToken, ImapFolder? folder, string format, params object[] args) { return QueueCommand (cancellationToken, folder, FormatOptions.Default, format, args); } @@ -1840,12 +3340,10 @@ public ImapCommand QueueCommand (CancellationToken cancellationToken, ImapFolder /// The IMAP command. public void QueueCommand (ImapCommand ic) { - if (IsBusy) - throw new InvalidOperationException ("The ImapClient is currently busy processing a command in another thread. Lock the SyncRoot property to properly synchronize your threads."); - - ic.Status = ImapCommandStatus.Queued; - ic.Id = nextId++; - queue.Add (ic); + lock (queue) { + ic.Status = ImapCommandStatus.Queued; + queue.Add (ic); + } } /// @@ -1855,13 +3353,21 @@ public void QueueCommand (ImapCommand ic) /// The cancellation token. public ImapCommandResponse QueryCapabilities (CancellationToken cancellationToken) { - if (Stream == null) - throw new InvalidOperationException (); + var ic = QueueCommand (cancellationToken, null, "CAPABILITY\r\n"); + + return Run (ic); + } + /// + /// Queries the capabilities. + /// + /// The command result. + /// The cancellation token. + public Task QueryCapabilitiesAsync (CancellationToken cancellationToken) + { var ic = QueueCommand (cancellationToken, null, "CAPABILITY\r\n"); - Wait (ic); - return ic.Response; + return RunAsync (ic); } /// @@ -1873,184 +3379,508 @@ public void CacheFolder (ImapFolder folder) if ((folder.Attributes & FolderAttributes.Inbox) != 0) cacheComparer.DirectorySeparator = folder.DirectorySeparator; - FolderCache.Add (folder.EncodedName, folder); + FolderCache.Add (folder.EncodedName, folder); + } + + /// + /// Gets the cached folder. + /// + /// if the folder was retrieved from the cache; otherwise, . + /// The encoded folder name. + /// The cached folder. + public bool TryGetCachedFolder (string encodedName, [NotNullWhen (true)] out ImapFolder? folder) + { + return FolderCache.TryGetValue (encodedName, out folder); + } + + bool RequiresParentLookup (ImapFolder folder, [NotNullWhen (true)] out string? encodedParentName) + { + encodedParentName = null; + + if (folder.ParentFolder != null) + return false; + + int index; + + // FIXME: should this search EncodedName instead of FullName? + if ((index = folder.FullName.LastIndexOf (folder.DirectorySeparator)) != -1) { + if (index == 0) + return false; + + var parentName = folder.FullName.Substring (0, index); + encodedParentName = EncodeMailboxName (parentName); + } else { + encodedParentName = string.Empty; + } + + if (TryGetCachedFolder (encodedParentName, out var parent)) { + folder.ParentFolder = parent; + return false; + } + + return true; + } + + ImapCommand QueueLookupParentFolderCommand (string encodedName, CancellationToken cancellationToken) + { + // Note: folder names can contain wildcards (including '*' and '%'), so replace '*' with '%' + // in order to reduce the list of folders returned by our LIST command. + var pattern = encodedName.Replace ('*', '%'); + var command = new StringBuilder ("LIST \"\" %S"); + var returnsSubscribed = false; + + if ((Capabilities & ImapCapabilities.ListExtended) != 0) { + // Try to get the \Subscribed and \HasChildren or \HasNoChildren attributes + command.Append (" RETURN (SUBSCRIBED CHILDREN)"); + returnsSubscribed = true; + } + + command.Append ("\r\n"); + + var ic = new ImapCommand (this, cancellationToken, null, command.ToString (), pattern); + ic.RegisterUntaggedHandler ("LIST", ImapUtils.UntaggedListHandler); + ic.ListReturnsSubscribed = returnsSubscribed; + ic.UserData = new List (); + + QueueCommand (ic); + + return ic; + } + + void ProcessLookupParentFolderResponse (ImapCommand ic, List list, ImapFolder folder, string encodedParentName) + { + if (!TryGetCachedFolder (encodedParentName, out var parent)) { + parent = CreateImapFolder (encodedParentName, FolderAttributes.NonExistent, folder.DirectorySeparator); + CacheFolder (parent); + } else if (parent.ParentFolder == null && !parent.IsNamespace) { + list.Add (parent); + } + + folder.ParentFolder = parent; + } + + /// + /// Looks up and sets the property of each of the folders. + /// + /// The IMAP folders. + /// The cancellation token. + internal void LookupParentFolders (IEnumerable folders, CancellationToken cancellationToken) + { + var list = new List (folders); + + // Note: we use a for-loop instead of foreach because we conditionally add items to the list. + for (int i = 0; i < list.Count; i++) { + var folder = list[i]; + + if (!RequiresParentLookup (folder, out var encodedParentName)) + continue; + + var ic = QueueLookupParentFolderCommand (encodedParentName, cancellationToken); + + Run (ic); + + ProcessLookupParentFolderResponse (ic, list, folder, encodedParentName); + } + } + + /// + /// Looks up and sets the property of each of the folders. + /// + /// The IMAP folders. + /// The cancellation token. + internal async Task LookupParentFoldersAsync (IEnumerable folders, CancellationToken cancellationToken) + { + var list = new List (folders); + + // Note: we use a for-loop instead of foreach because we conditionally add items to the list. + for (int i = 0; i < list.Count; i++) { + var folder = list[i]; + + if (!RequiresParentLookup (folder, out var encodedParentName)) + continue; + + var ic = QueueLookupParentFolderCommand (encodedParentName, cancellationToken); + + await RunAsync (ic).ConfigureAwait (false); + + ProcessLookupParentFolderResponse (ic, list, folder, encodedParentName); + } + } + + void ProcessNamespaceResponse (ImapCommand ic) + { + if (QuirksMode == ImapQuirksMode.Exchange && ic.Response == ImapCommandResponse.Bad) { + State = ImapEngineState.Connected; // Reset back to Connected-but-not-Authenticated state + throw ImapCommandException.Create ("NAMESPACE", ic); + } + } + + ImapCommand QueueListNamespaceCommand (List list, CancellationToken cancellationToken) + { + var ic = new ImapCommand (this, cancellationToken, null, "LIST \"\" \"\"\r\n"); + ic.RegisterUntaggedHandler ("LIST", ImapUtils.UntaggedListHandler); + ic.UserData = list; + + QueueCommand (ic); + + return ic; + } + + void ProcessListNamespaceResponse (ImapCommand ic, List list) + { + PersonalNamespaces.Clear (); + SharedNamespaces.Clear (); + OtherNamespaces.Clear (); + + if (list.Count > 0) { + var empty = list.FirstOrDefault (x => x.EncodedName.Length == 0); + + if (empty == null) { + empty = CreateImapFolder (string.Empty, FolderAttributes.None, list[0].DirectorySeparator); + CacheFolder (empty); + } + + PersonalNamespaces.Add (new FolderNamespace (empty.DirectorySeparator, empty.FullName)); + empty.UpdateIsNamespace (true); + } + } + + /// + /// Queries the namespaces. + /// + /// The command result. + /// The cancellation token. + public ImapCommandResponse QueryNamespaces (CancellationToken cancellationToken) + { + ImapCommand ic; + + // Note: It seems that on Exchange 2003 (maybe Chinese-only version?), the NAMESPACE command causes the server + // to immediately drop the connection. Avoid this issue by not using the NAMESPACE command if we detect that + // the server is Microsoft Exchange 2003. See https://github.com/jstedfast/MailKit/issues/1512 for details. + if (QuirksMode != ImapQuirksMode.Exchange2003 && (Capabilities & ImapCapabilities.Namespace) != 0) { + ic = QueueCommand (cancellationToken, null, "NAMESPACE\r\n"); + + Run (ic); + + ProcessNamespaceResponse (ic); + } else { + var list = new List (); + + ic = QueueListNamespaceCommand (list, cancellationToken); + + Run (ic); + + ProcessListNamespaceResponse (ic, list); + + LookupParentFolders (list, cancellationToken); + } + + return ic.Response; + } + + /// + /// Asynchronously queries the namespaces. + /// + /// The command result. + /// The cancellation token. + public async Task QueryNamespacesAsync (CancellationToken cancellationToken) + { + ImapCommand ic; + + // Note: It seems that on Exchange 2003 (maybe Chinese-only version?), the NAMESPACE command causes the server + // to immediately drop the connection. Avoid this issue by not using the NAMESPACE command if we detect that + // the server is Microsoft Exchange 2003. See https://github.com/jstedfast/MailKit/issues/1512 for details. + if (QuirksMode != ImapQuirksMode.Exchange2003 && (Capabilities & ImapCapabilities.Namespace) != 0) { + ic = QueueCommand (cancellationToken, null, "NAMESPACE\r\n"); + + await RunAsync (ic).ConfigureAwait (false); + + ProcessNamespaceResponse (ic); + } else { + var list = new List (); + + ic = QueueListNamespaceCommand (list, cancellationToken); + + await RunAsync (ic).ConfigureAwait (false); + + ProcessListNamespaceResponse (ic, list); + + await LookupParentFoldersAsync (list, cancellationToken).ConfigureAwait (false); + } + + return ic.Response; + } + + internal static ImapFolder? GetFolder (List folders, string encodedName) + { + for (int i = 0; i < folders.Count; i++) { + if (encodedName.Equals (folders[i].EncodedName, StringComparison.OrdinalIgnoreCase)) + return folders[i]; + } + + return null; + } + + /// + /// Assigns a folder as a special folder. + /// + /// The special folder. + public void AssignSpecialFolder (ImapFolder folder) + { + if ((folder.Attributes & FolderAttributes.All) != 0) + All = folder; + if ((folder.Attributes & FolderAttributes.Archive) != 0) + Archive = folder; + if ((folder.Attributes & FolderAttributes.Drafts) != 0) + Drafts = folder; + if ((folder.Attributes & FolderAttributes.Flagged) != 0) + Flagged = folder; + if ((folder.Attributes & FolderAttributes.Important) != 0) + Important = folder; + if ((folder.Attributes & FolderAttributes.Junk) != 0) + Junk = folder; + if ((folder.Attributes & FolderAttributes.Sent) != 0) + Sent = folder; + if ((folder.Attributes & FolderAttributes.Trash) != 0) + Trash = folder; + } + + /// + /// Assigns the special folders. + /// + /// The list of folders. + public void AssignSpecialFolders (IList list) + { + for (int i = 0; i < list.Count; i++) + AssignSpecialFolder (list[i]); + } + + ImapCommand QueueListInboxCommand (CancellationToken cancellationToken, out StringBuilder command, out List list) + { + bool returnsSubscribed = false; + + command = new StringBuilder ("LIST \"\" \"INBOX\""); + list = new List (); + + if ((Capabilities & ImapCapabilities.ListExtended) != 0) { + command.Append (" RETURN (SUBSCRIBED CHILDREN)"); + returnsSubscribed = true; + } + + command.Append ("\r\n"); + + var ic = new ImapCommand (this, cancellationToken, null, command.ToString ()); + ic.RegisterUntaggedHandler ("LIST", ImapUtils.UntaggedListHandler); + ic.ListReturnsSubscribed = returnsSubscribed; + ic.UserData = list; + + QueueCommand (ic); + + return ic; + } + + void ProcessListInboxResponse (ImapCommand ic, StringBuilder command, List list) + { + TryGetCachedFolder ("INBOX", out var folder); + Inbox = folder; + + command.Clear (); + list.Clear (); + } + + ImapCommand QueueListSpecialUseCommand (StringBuilder command, List list, CancellationToken cancellationToken) + { + bool returnsSubscribed = false; + + command.Append ("LIST "); + + // Note: Some IMAP servers like ProtonMail respond to SPECIAL-USE LIST queries with BAD, so fall + // back to just issuing a standard LIST command and hope we get back some SPECIAL-USE attributes. + // + // See https://github.com/jstedfast/MailKit/issues/674 for details. + if (QuirksMode != ImapQuirksMode.ProtonMail) + command.Append ("(SPECIAL-USE) \"\" \"*\""); + else + command.Append ("\"\" \"%%\""); + + if ((Capabilities & ImapCapabilities.ListExtended) != 0) { + command.Append (" RETURN (SUBSCRIBED CHILDREN)"); + returnsSubscribed = true; + } + + command.Append ("\r\n"); + + var ic = new ImapCommand (this, cancellationToken, null, command.ToString ()); + ic.RegisterUntaggedHandler ("LIST", ImapUtils.UntaggedListHandler); + ic.ListReturnsSubscribed = returnsSubscribed; + ic.UserData = list; + + QueueCommand (ic); + + return ic; } - /// - /// Gets the cached folder. - /// - /// true if the folder was retreived from the cache; otherwise, false. - /// The encoded folder name. - /// The cached folder. - public bool GetCachedFolder (string encodedName, out ImapFolder folder) + ImapCommand QueueXListCommand (List list, CancellationToken cancellationToken) { - return FolderCache.TryGetValue (encodedName, out folder); + var ic = new ImapCommand (this, cancellationToken, null, "XLIST \"\" \"*\"\r\n"); + ic.RegisterUntaggedHandler ("XLIST", ImapUtils.UntaggedListHandler); + ic.UserData = list; + + QueueCommand (ic); + + return ic; } /// - /// Looks up and sets the property of each of the folders. + /// Queries the special folders. /// - /// The IMAP folders. /// The cancellation token. - void LookupParentFolders (IEnumerable folders, CancellationToken cancellationToken) + public void QuerySpecialFolders (CancellationToken cancellationToken) { - var list = new List (folders); - string encodedName; - ImapFolder parent; - int index; + var ic = QueueListInboxCommand (cancellationToken, out var command, out var list); - // Note: we use a for-loop instead of foreach because we conditionally add items to the list. - for (int i = 0; i < list.Count; i++) { - var folder = list[i]; + Run (ic); - if (folder.ParentFolder != null) - continue; + ProcessListInboxResponse (ic, command, list); + + if (Inbox == null) { + if ((Capabilities & ImapCapabilities.ListExtended) != 0) { + // Note: This is a work-around for IMAP servers such as imap.strato.de which do not return a list of folders + // for the `LIST "" "INBOX" RETURN (SUBSCRIBED CHILDREN)` command. Disable the LIST-EXTENDED (and dependent) + // capabilities since they are clearly broken. + // + // See https://github.com/jstedfast/MailKit/issues/1957 for details. + Capabilities &= ~(ImapCapabilities.ListExtended | ImapCapabilities.ListStatus | ImapCapabilities.SpecialUse); - if ((index = folder.FullName.LastIndexOf (folder.DirectorySeparator)) != -1) { - if (index == 0) - continue; + // Send a vanilla `LIST "" "INBOX"` command to get the INBOX folder. + ic = QueueListInboxCommand (cancellationToken, out command, out list); - var parentName = folder.FullName.Substring (0, index); - encodedName = EncodeMailboxName (parentName); - } else { - encodedName = string.Empty; + Run (ic); + + ProcessListInboxResponse (ic, command, list); } - if (GetCachedFolder (encodedName, out parent)) { - folder.ParentFolder = parent; - continue; + if (Inbox == null) { + // If we still don't have the INBOX folder, just create a placeholder for it. + char delim = PersonalNamespaces.Count > 0 ? PersonalNamespaces[0].DirectorySeparator : '/'; + var inbox = CreateImapFolder ("INBOX", FolderAttributes.Inbox, delim); + CacheFolder (inbox); + Inbox = inbox; } + } - var ic = new ImapCommand (this, cancellationToken, null, "LIST \"\" %S\r\n", encodedName); - ic.RegisterUntaggedHandler ("LIST", ImapUtils.ParseFolderList); - ic.UserData = new List (); + if ((Capabilities & ImapCapabilities.SpecialUse) != 0) { + ic = QueueListSpecialUseCommand (command, list, cancellationToken); - QueueCommand (ic); - Wait (ic); + Run (ic); - if (!GetCachedFolder (encodedName, out parent)) { - parent = CreateImapFolder (encodedName, FolderAttributes.NonExistent, folder.DirectorySeparator); - CacheFolder (parent); - } else if (parent.ParentFolder == null && !parent.IsNamespace) { - list.Add (parent); - } + // Note: We specifically don't throw if we get a LIST error. + } else if ((Capabilities & ImapCapabilities.XList) != 0) { + ic = QueueXListCommand (list, cancellationToken); - folder.ParentFolder = parent; + Run (ic); + + // Note: We specifically don't throw if we get a XLIST error. } + + LookupParentFolders (list, cancellationToken); + + AssignSpecialFolders (list); } /// - /// Queries the namespaces. + /// Queries the special folders. /// - /// The command result. /// The cancellation token. - public ImapCommandResponse QueryNamespaces (CancellationToken cancellationToken) + public async Task QuerySpecialFoldersAsync (CancellationToken cancellationToken) { - if (Stream == null) - throw new InvalidOperationException (); + var ic = QueueListInboxCommand (cancellationToken, out var command, out var list); - ImapCommand ic; + await RunAsync (ic).ConfigureAwait (false); - if ((Capabilities & ImapCapabilities.Namespace) != 0) { - ic = QueueCommand (cancellationToken, null, "NAMESPACE\r\n"); - Wait (ic); - } else { - var list = new List (); + ProcessListInboxResponse (ic, command, list); - ic = new ImapCommand (this, cancellationToken, null, "LIST \"\" \"\"\r\n"); - ic.RegisterUntaggedHandler ("LIST", ImapUtils.ParseFolderList); - ic.UserData = list; + if (Inbox == null) { + if ((Capabilities & ImapCapabilities.ListExtended) != 0) { + // Note: This is a work-around for IMAP servers such as imap.strato.de which do not return a list of folders + // for the `LIST "" "INBOX" RETURN (SUBSCRIBED CHILDREN)` command. Disable the LIST-EXTENDED (and dependent) + // capabilities since they are clearly broken. + // + // See https://github.com/jstedfast/MailKit/issues/1957 for details. + Capabilities &= ~(ImapCapabilities.ListExtended | ImapCapabilities.ListStatus | ImapCapabilities.SpecialUse); - QueueCommand (ic); - Wait (ic); + // Send a vanilla `LIST "" "INBOX"` command to get the INBOX folder. + ic = QueueListInboxCommand (cancellationToken, out command, out list); - PersonalNamespaces.Clear (); - SharedNamespaces.Clear (); - OtherNamespaces.Clear (); + await RunAsync (ic).ConfigureAwait (false); - if (list.Count > 0) { - PersonalNamespaces.Add (new FolderNamespace (list[0].DirectorySeparator, "")); - list[0].UpdateIsNamespace (true); + ProcessListInboxResponse (ic, command, list); } - LookupParentFolders (list, cancellationToken); + if (Inbox == null) { + // If we still don't have the INBOX folder, just create a placeholder for it. + char delim = PersonalNamespaces.Count > 0 ? PersonalNamespaces[0].DirectorySeparator : '/'; + var inbox = CreateImapFolder ("INBOX", FolderAttributes.Inbox, delim); + CacheFolder (inbox); + Inbox = inbox; + } } - return ic.Response; - } + if ((Capabilities & ImapCapabilities.SpecialUse) != 0) { + ic = QueueListSpecialUseCommand (command, list, cancellationToken); - /// - /// Assigns the special folders. - /// - /// The list of folders. - public void AssignSpecialFolders (IList list) - { - for (int i = 0; i < list.Count; i++) { - var folder = list[i]; + await RunAsync (ic).ConfigureAwait (false); + + // Note: We specifically don't throw if we get a LIST error. + } else if ((Capabilities & ImapCapabilities.XList) != 0) { + ic = QueueXListCommand (list, cancellationToken); - if ((folder.Attributes & FolderAttributes.All) != 0) - All = folder; - if ((folder.Attributes & FolderAttributes.Archive) != 0) - Archive = folder; - if ((folder.Attributes & FolderAttributes.Drafts) != 0) - Drafts = folder; - if ((folder.Attributes & FolderAttributes.Flagged) != 0) - Flagged = folder; - if ((folder.Attributes & FolderAttributes.Junk) != 0) - Junk = folder; - if ((folder.Attributes & FolderAttributes.Sent) != 0) - Sent = folder; - if ((folder.Attributes & FolderAttributes.Trash) != 0) - Trash = folder; + await RunAsync (ic).ConfigureAwait (false); + + // Note: We specifically don't throw if we get a LIST error. } + + await LookupParentFoldersAsync (list, cancellationToken).ConfigureAwait (false); + + AssignSpecialFolders (list); } - /// - /// Queries the special folders. - /// - /// The cancellation token. - public void QuerySpecialFolders (CancellationToken cancellationToken) + ImapFolder ProcessGetQuotaRootResponse (ImapCommand ic, string quotaRoot, out List list) { - if (Stream == null) - throw new InvalidOperationException (); + ImapFolder? folder; - var list = new List (); - ImapFolder folder; - ImapCommand ic; + list = (List) ic.UserData!; - ic = new ImapCommand (this, cancellationToken, null, "LIST \"\" \"INBOX\"\r\n"); - ic.RegisterUntaggedHandler ("LIST", ImapUtils.ParseFolderList); - ic.UserData = list; + ic.ThrowIfNotOk ("LIST"); - QueueCommand (ic); - Wait (ic); + if ((folder = GetFolder (list, quotaRoot)) == null) { + folder = CreateImapFolder (quotaRoot, FolderAttributes.NonExistent, '.'); + CacheFolder (folder); + } - GetCachedFolder ("INBOX", out folder); - Inbox = folder; + return folder; + } - list.Clear (); + /// + /// Gets the folder representing the specified quota root. + /// + /// The folder. + /// The name of the quota root. + /// The cancellation token. + public ImapFolder GetQuotaRootFolder (string quotaRoot, CancellationToken cancellationToken) + { + if (TryGetCachedFolder (quotaRoot, out var folder)) + return folder; - if ((Capabilities & ImapCapabilities.SpecialUse) != 0) { - ic = new ImapCommand (this, cancellationToken, null, "LIST (SPECIAL-USE) \"\" \"*\"\r\n"); - ic.RegisterUntaggedHandler ("LIST", ImapUtils.ParseFolderList); - ic.UserData = list; + var ic = QueueGetFolderCommand (quotaRoot, cancellationToken); - QueueCommand (ic); - Wait (ic); + Run (ic); - LookupParentFolders (list, cancellationToken); - AssignSpecialFolders (list); - } else if ((Capabilities & ImapCapabilities.XList) != 0) { - ic = new ImapCommand (this, cancellationToken, null, "XLIST \"\" \"*\"\r\n"); - ic.RegisterUntaggedHandler ("XLIST", ImapUtils.ParseFolderList); - ic.UserData = list; + folder = ProcessGetQuotaRootResponse (ic, quotaRoot, out var list); - QueueCommand (ic); - Wait (ic); + LookupParentFolders (list, cancellationToken); - LookupParentFolders (list, cancellationToken); - AssignSpecialFolders (list); - } + return folder; } /// @@ -2059,35 +3889,57 @@ public void QuerySpecialFolders (CancellationToken cancellationToken) /// The folder. /// The name of the quota root. /// The cancellation token. - public ImapFolder GetQuotaRootFolder (string quotaRoot, CancellationToken cancellationToken) + public async Task GetQuotaRootFolderAsync (string quotaRoot, CancellationToken cancellationToken) + { + if (TryGetCachedFolder (quotaRoot, out var folder)) + return folder; + + var ic = QueueGetFolderCommand (quotaRoot, cancellationToken); + + await RunAsync (ic).ConfigureAwait (false); + + folder = ProcessGetQuotaRootResponse (ic, quotaRoot, out var list); + + await LookupParentFoldersAsync (list, cancellationToken).ConfigureAwait (false); + + return folder; + } + + ImapCommand QueueGetFolderCommand (string encodedName, CancellationToken cancellationToken) { + var command = new StringBuilder ("LIST \"\" %S"); var list = new List (); - ImapFolder folder; + var returnsSubscribed = false; - if (GetCachedFolder (quotaRoot, out folder)) - return folder; + if ((Capabilities & ImapCapabilities.ListExtended) != 0) { + command.Append (" RETURN (SUBSCRIBED CHILDREN)"); + returnsSubscribed = true; + } - var ic = new ImapCommand (this, cancellationToken, null, "LIST \"\" %S\r\n", quotaRoot); - ic.RegisterUntaggedHandler ("LIST", ImapUtils.ParseFolderList); + command.Append ("\r\n"); + + var ic = new ImapCommand (this, cancellationToken, null, command.ToString (), encodedName); + ic.RegisterUntaggedHandler ("LIST", ImapUtils.UntaggedListHandler); + ic.ListReturnsSubscribed = returnsSubscribed; ic.UserData = list; QueueCommand (ic); - Wait (ic); - ProcessResponseCodes (ic); + return ic; + } - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("LIST", ic); + static ImapFolder ProcessGetFolderResponse (ImapCommand ic, string path, string encodedName, out List list) + { + ImapFolder? folder; - if (list.Count == 0) { - folder = CreateImapFolder (quotaRoot, FolderAttributes.NonExistent, '.'); - CacheFolder (folder); - return folder; - } + list = (List) ic.UserData!; - LookupParentFolders (list, cancellationToken); + ic.ThrowIfNotOk ("LIST"); + + if ((folder = GetFolder (list, encodedName)) == null) + throw new FolderNotFoundException (path); - return list[0]; + return folder; } /// @@ -2099,30 +3951,43 @@ public ImapFolder GetQuotaRootFolder (string quotaRoot, CancellationToken cancel public ImapFolder GetFolder (string path, CancellationToken cancellationToken) { var encodedName = EncodeMailboxName (path); - var list = new List (); - ImapFolder folder; - if (GetCachedFolder (encodedName, out folder)) + if (TryGetCachedFolder (encodedName, out var folder)) return folder; - var ic = new ImapCommand (this, cancellationToken, null, "LIST \"\" %S\r\n", encodedName); - ic.RegisterUntaggedHandler ("LIST", ImapUtils.ParseFolderList); - ic.UserData = list; + var ic = QueueGetFolderCommand (encodedName, cancellationToken); - QueueCommand (ic); - Wait (ic); + Run (ic); - ProcessResponseCodes (ic); + folder = ProcessGetFolderResponse (ic, path, encodedName, out var list); - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("LIST", ic); + LookupParentFolders (list, cancellationToken); - if (list.Count == 0) - throw new FolderNotFoundException (path); + return folder; + } - LookupParentFolders (list, cancellationToken); + /// + /// Gets the folder for the specified path. + /// + /// The folder. + /// The folder path. + /// The cancellation token. + public async Task GetFolderAsync (string path, CancellationToken cancellationToken) + { + var encodedName = EncodeMailboxName (path); + + if (TryGetCachedFolder (encodedName, out var folder)) + return folder; + + var ic = QueueGetFolderCommand (encodedName, cancellationToken); - return list[0]; + await RunAsync (ic).ConfigureAwait (false); + + folder = ProcessGetFolderResponse (ic, path, encodedName, out var list); + + await LookupParentFoldersAsync (list, cancellationToken).ConfigureAwait (false); + + return folder; } internal string GetStatusQuery (StatusItems items) @@ -2152,36 +4017,37 @@ internal string GetStatusQuery (StatusItems items) flags += "APPENDLIMIT "; } + if ((Capabilities & ImapCapabilities.StatusSize) != 0) { + if ((items & StatusItems.Size) != 0) + flags += "SIZE "; + } + + if ((Capabilities & ImapCapabilities.ObjectID) != 0) { + if ((items & StatusItems.MailboxId) != 0) + flags += "MAILBOXID "; + } + return flags.TrimEnd (); } - /// - /// Get all of the folders within the specified namespace. - /// - /// - /// Gets all of the folders within the specified namespace. - /// - /// The list of folders. - /// The namespace. - /// The status items to pre-populate. - /// If set to true, only subscribed folders will be listed. - /// The cancellation token. - public IList GetFolders (FolderNamespace @namespace, StatusItems items, bool subscribedOnly, CancellationToken cancellationToken) + ImapCommand QueueGetFoldersCommand (FolderNamespace @namespace, StatusItems items, bool subscribedOnly, CancellationToken cancellationToken, out bool status) { var encodedName = EncodeMailboxName (@namespace.Path); var pattern = encodedName.Length > 0 ? encodedName + @namespace.DirectorySeparator : string.Empty; - var status = items != StatusItems.None; var list = new List (); var command = new StringBuilder (); + var returnsSubscribed = false; var lsub = subscribedOnly; - ImapFolder folder; - if (!GetCachedFolder (encodedName, out folder)) + status = items != StatusItems.None; + + if (!TryGetCachedFolder (encodedName, out var folder)) throw new FolderNotFoundException (@namespace.Path); if (subscribedOnly) { if ((Capabilities & ImapCapabilities.ListExtended) != 0) { command.Append ("LIST (SUBSCRIBED)"); + returnsSubscribed = true; lsub = false; } else { command.Append ("LSUB"); @@ -2197,18 +4063,23 @@ public IList GetFolders (FolderNamespace @namespace, StatusItems ite command.Append (" RETURN ("); if ((Capabilities & ImapCapabilities.ListExtended) != 0) { - if (!subscribedOnly) + if (!subscribedOnly) { command.Append ("SUBSCRIBED "); + returnsSubscribed = true; + } command.Append ("CHILDREN "); } - command.AppendFormat ("STATUS ({0})", GetStatusQuery (items)); - command.Append (')'); + command.Append ("STATUS ("); + command.Append (GetStatusQuery (items)); + command.Append ("))"); status = false; } else if ((Capabilities & ImapCapabilities.ListExtended) != 0) { command.Append (" RETURN ("); - if (!subscribedOnly) + if (!subscribedOnly) { command.Append ("SUBSCRIBED "); + returnsSubscribed = true; + } command.Append ("CHILDREN"); command.Append (')'); } @@ -2217,31 +4088,87 @@ public IList GetFolders (FolderNamespace @namespace, StatusItems ite command.Append ("\r\n"); var ic = new ImapCommand (this, cancellationToken, null, command.ToString (), pattern + "*"); - ic.RegisterUntaggedHandler (lsub ? "LSUB" : "LIST", ImapUtils.ParseFolderList); + ic.RegisterUntaggedHandler (lsub ? "LSUB" : "LIST", ImapUtils.UntaggedListHandler); + ic.ListReturnsSubscribed = returnsSubscribed; ic.UserData = list; + ic.Lsub = lsub; QueueCommand (ic); - Wait (ic); - if (lsub) { - // the LSUB command does not send \Subscribed flags so we need to add them ourselves - for (int i = 0; i < list.Count; i++) - list[i].Attributes |= FolderAttributes.Subscribed; - } + return ic; + } - ProcessResponseCodes (ic); + static IList ToListOfIMailFolder (List list) + { + var folders = new IMailFolder[list.Count]; + for (int i = 0; i < folders.Length; i++) + folders[i] = list[i]; + + return folders; + } + + /// + /// Get all of the folders within the specified namespace. + /// + /// + /// Gets all of the folders within the specified namespace. + /// + /// The list of folders. + /// The namespace. + /// The status items to pre-populate. + /// If set to , only subscribed folders will be listed. + /// The cancellation token. + public IList GetFolders (FolderNamespace @namespace, StatusItems items, bool subscribedOnly, CancellationToken cancellationToken) + { + var ic = QueueGetFoldersCommand (@namespace, items, subscribedOnly, cancellationToken, out bool status); + var list = (List) ic.UserData!; - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create (lsub ? "LSUB" : "LIST", ic); + Run (ic); + + ic.ThrowIfNotOk (ic.Lsub ? "LSUB" : "LIST"); LookupParentFolders (list, cancellationToken); if (status) { - for (int i = 0; i < list.Count; i++) - list[i].Status (items, cancellationToken); + for (int i = 0; i < list.Count; i++) { + if (list[i].Exists) + list[i].Status (items, false, cancellationToken); + } + } + + return ToListOfIMailFolder (list); + } + + /// + /// Get all of the folders within the specified namespace. + /// + /// + /// Gets all of the folders within the specified namespace. + /// + /// The list of folders. + /// The namespace. + /// The status items to pre-populate. + /// If set to , only subscribed folders will be listed. + /// The cancellation token. + public async Task> GetFoldersAsync (FolderNamespace @namespace, StatusItems items, bool subscribedOnly, CancellationToken cancellationToken) + { + var ic = QueueGetFoldersCommand (@namespace, items, subscribedOnly, cancellationToken, out bool status); + var list = (List) ic.UserData!; + + await RunAsync (ic).ConfigureAwait (false); + + ic.ThrowIfNotOk (ic.Lsub ? "LSUB" : "LIST"); + + await LookupParentFoldersAsync (list, cancellationToken).ConfigureAwait (false); + + if (status) { + for (int i = 0; i < list.Count; i++) { + if (list[i].Exists) + await list[i].StatusAsync (items, false, cancellationToken).ConfigureAwait (false); + } } - return list; + return ToListOfIMailFolder (list); } /// @@ -2267,10 +4194,10 @@ public string EncodeMailboxName (string mailboxName) /// /// Determines whether the mailbox name is valid or not. /// - /// true if the mailbox name is valid; otherwise, false. + /// if the mailbox name is valid; otherwise, . /// The mailbox name. - /// The path delimeter. - public bool IsValidMailboxName (string mailboxName, char delim) + /// The path delimiter. + public static bool IsValidMailboxName (string mailboxName, char delim) { // From rfc6855: // @@ -2288,57 +4215,115 @@ public bool IsValidMailboxName (string mailboxName, char delim) return mailboxName.Length > 0; } - public HeaderList ParseHeaders (Stream stream, CancellationToken cancellationToken) + [MemberNotNull (nameof (parser))] + void InitializeParser (Stream stream, bool persistent) { if (parser == null) - parser = new MimeParser (ParserOptions.Default, stream); + parser = new MimeParser (ParserOptions.Default, stream, persistent); else - parser.SetStream (ParserOptions.Default, stream); + parser.SetStream (stream, persistent); + } + + public HeaderList ParseHeaders (Stream stream, CancellationToken cancellationToken) + { + InitializeParser (stream, false); return parser.ParseHeaders (cancellationToken); } + public Task ParseHeadersAsync (Stream stream, CancellationToken cancellationToken) + { + InitializeParser (stream, false); + + return parser.ParseHeadersAsync (cancellationToken); + } + public MimeMessage ParseMessage (Stream stream, bool persistent, CancellationToken cancellationToken) { - if (parser == null) - parser = new MimeParser (ParserOptions.Default, stream, persistent); - else - parser.SetStream (ParserOptions.Default, stream, persistent); + InitializeParser (stream, persistent); return parser.ParseMessage (cancellationToken); } + public Task ParseMessageAsync (Stream stream, bool persistent, CancellationToken cancellationToken) + { + InitializeParser (stream, persistent); + + return parser.ParseMessageAsync (cancellationToken); + } + public MimeEntity ParseEntity (Stream stream, bool persistent, CancellationToken cancellationToken) { - if (parser == null) - parser = new MimeParser (ParserOptions.Default, stream, persistent); - else - parser.SetStream (ParserOptions.Default, stream, persistent); + InitializeParser (stream, persistent); return parser.ParseEntity (cancellationToken); } + public Task ParseEntityAsync (Stream stream, bool persistent, CancellationToken cancellationToken) + { + InitializeParser (stream, persistent); + + return parser.ParseEntityAsync (cancellationToken); + } + /// /// Occurs when the engine receives an alert message from the server. /// - public event EventHandler Alert; + public event EventHandler? Alert; internal void OnAlert (string message) { - var handler = Alert; + Alert?.Invoke (this, new AlertEventArgs (message)); + } - if (handler != null) - handler (this, new AlertEventArgs (message)); + /// + /// Occurs when the engine receives a webalert message from the server. + /// + public event EventHandler? WebAlert; + + internal void OnWebAlert (Uri uri, string message) + { + WebAlert?.Invoke (this, new WebAlertEventArgs (uri, message)); } - public event EventHandler Disconnected; + /// + /// Occurs when the engine receives a notification that a folder has been created. + /// + public event EventHandler? FolderCreated; - void OnDisconnected () + internal void OnFolderCreated (IMailFolder folder) + { + FolderCreated?.Invoke (this, new FolderCreatedEventArgs (folder)); + } + + /// + /// Occurs when the engine receives a notification that metadata has changed. + /// + public event EventHandler? MetadataChanged; + + internal void OnMetadataChanged (Metadata metadata) + { + MetadataChanged?.Invoke (this, new MetadataChangedEventArgs (metadata)); + } + + /// + /// Occurs when the engine receives a notification overflow message from the server. + /// + public event EventHandler? NotificationOverflow; + + internal void OnNotificationOverflow () { - var handler = Disconnected; + // [NOTIFICATIONOVERFLOW] will reset to NOTIFY NONE + NotifySelectedNewExpunge = false; + + NotificationOverflow?.Invoke (this, EventArgs.Empty); + } + + public event EventHandler? Disconnected; - if (handler != null) - handler (this, EventArgs.Empty); + void OnDisconnected () + { + Disconnected?.Invoke (this, EventArgs.Empty); } /// @@ -2351,7 +4336,7 @@ void OnDisconnected () public void Dispose () { disposed = true; - Disconnect (); + Disconnect (null); } } } diff --git a/MailKit/Net/Imap/ImapEventGroup.cs b/MailKit/Net/Imap/ImapEventGroup.cs new file mode 100644 index 0000000000..6f93ba65a8 --- /dev/null +++ b/MailKit/Net/Imap/ImapEventGroup.cs @@ -0,0 +1,732 @@ +// +// ImapFolderFetch.cs +// +// Authors: Steffen Kieß +// Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Text; +using System.Collections.Generic; + +using MimeKit; + +namespace MailKit.Net.Imap { + /// + /// An IMAP event group used with the NOTIFY command. + /// + /// + /// An IMAP event group used with the NOTIFY command. + /// + public sealed class ImapEventGroup + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The mailbox filter. + /// The list of IMAP events. + /// + /// is . + /// -or- + /// is . + /// + public ImapEventGroup (ImapMailboxFilter mailboxFilter, IList events) + { + if (mailboxFilter == null) + throw new ArgumentNullException (nameof (mailboxFilter)); + + if (events == null) + throw new ArgumentNullException (nameof (events)); + + MailboxFilter = mailboxFilter; + Events = events; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The mailbox filter. + /// The list of IMAP events. + /// + /// is . + /// -or- + /// is . + /// + public ImapEventGroup (ImapMailboxFilter mailboxFilter, params ImapEvent[] events) : this (mailboxFilter, (IList) events) + { + } + + /// + /// Get the mailbox filter. + /// + /// + /// Gets the mailbox filter. + /// + /// The mailbox filter. + public ImapMailboxFilter MailboxFilter { + get; private set; + } + + /// + /// Get the list of IMAP events. + /// + /// + /// Gets the list of IMAP events. + /// + /// The events. + public IList Events { + get; private set; + } + + /// + /// Format the IMAP NOTIFY command for this particular IMAP event group. + /// + /// + /// Formats the IMAP NOTIFY command for this particular IMAP event group. + /// + /// The IMAP engine. + /// The IMAP command builder. + /// The IMAP command argument builder. + /// Gets set to if the NOTIFY command requests the MessageNew or + /// MessageExpunged events for a SELECTED or SELECTED-DELAYED mailbox filter; otherwise it is left unchanged. + internal void Format (ImapEngine engine, StringBuilder command, IList args, ref bool notifySelectedNewExpunge) + { + bool isSelectedFilter = MailboxFilter == ImapMailboxFilter.Selected || MailboxFilter == ImapMailboxFilter.SelectedDelayed; + + command.Append ('('); + MailboxFilter.Format (engine, command, args); + command.Append (' '); + + if (Events.Count > 0) { + var haveAnnotationChange = false; + var haveMessageExpunge = false; + var haveMessageNew = false; + var haveFlagChange = false; + + command.Append ('('); + + for (int i = 0; i < Events.Count; i++) { + var @event = Events[i]; + + if (isSelectedFilter && !@event.IsMessageEvent) + throw new InvalidOperationException ("Only message events may be specified when SELECTED or SELECTED-DELAYED is used."); + + if (@event is ImapEvent.MessageNew) + haveMessageNew = true; + else if (@event == ImapEvent.MessageExpunge) + haveMessageExpunge = true; + else if (@event == ImapEvent.FlagChange) + haveFlagChange = true; + else if (@event == ImapEvent.AnnotationChange) + haveAnnotationChange = true; + + if (i > 0) + command.Append (' '); + + @event.Format (engine, command, args, isSelectedFilter); + } + command.Append (')'); + + // https://tools.ietf.org/html/rfc5465#section-5 + if ((haveMessageNew && !haveMessageExpunge) || (!haveMessageNew && haveMessageExpunge)) + throw new InvalidOperationException ("If MessageNew or MessageExpunge is specified, both must be specified."); + + if ((haveFlagChange || haveAnnotationChange) && (!haveMessageNew || !haveMessageExpunge)) + throw new InvalidOperationException ("If FlagChange and/or AnnotationChange are specified, MessageNew and MessageExpunge must also be specified."); + + notifySelectedNewExpunge = (haveMessageNew || haveMessageExpunge) && MailboxFilter == ImapMailboxFilter.Selected; + } else { + command.Append ("NONE"); + } + + command.Append (')'); + } + } + + /// + /// An IMAP mailbox filter for use with the NOTIFY command. + /// + /// + /// An IMAP mailbox filter for use with the NOTIFY command. + /// + public class ImapMailboxFilter + { + /// + /// An IMAP mailbox filter specifying that the client wants immediate notifications for + /// the currently selected folder. + /// + /// + /// The SELECTED mailbox specifier requires the server to send immediate + /// notifications for the currently selected mailbox about all specified + /// message events. + /// + public static readonly ImapMailboxFilter Selected = new ImapMailboxFilter ("SELECTED"); + + /// + /// An IMAP mailbox filter specifying the currently selected folder but delays notifications + /// until a command has been issued. + /// + /// + /// The SELECTED-DELAYED mailbox specifier requires the server to delay a + /// event until the client issues a command that allows + /// returning information about expunged messages (see + /// Section 7.4.1 of RFC3501] + /// for more details), for example, till a NOOP or an IDLE command has been issued. + /// When SELECTED-DELAYED is specified, the server MAY also delay returning other message + /// events until the client issues one of the commands specified above, or it MAY return them + /// immediately. + /// + public static readonly ImapMailboxFilter SelectedDelayed = new ImapMailboxFilter ("SELECTED-DELAYED"); + + /// + /// An IMAP mailbox filter specifying the currently selected folder. + /// + /// + /// The INBOXES mailbox specifier refers to all selectable mailboxes in the user's + /// personal namespace(s) to which messages may be delivered by a Message Delivery Agent (MDA). + /// + /// If the IMAP server cannot easily compute this set, it MUST treat + /// as equivalent to . + /// + public static readonly ImapMailboxFilter Inboxes = new ImapMailboxFilter ("INBOXES"); + + /// + /// An IMAP mailbox filter specifying all selectable folders within the user's personal namespace. + /// + /// + /// The PERSONAL mailbox specifier refers to all selectable folders within the user's personal namespace. + /// + public static readonly ImapMailboxFilter Personal = new ImapMailboxFilter ("PERSONAL"); + + /// + /// An IMAP mailbox filter that refers to all subscribed folders. + /// + /// + /// The SUBSCRIBED mailbox specifier refers to all folders subscribed to by the user. + /// If the subscription list changes, the server MUST reevaluate the list. + /// + public static readonly ImapMailboxFilter Subscribed = new ImapMailboxFilter ("SUBSCRIBED"); + + /// + /// An IMAP mailbox filter that specifies a list of folders to receive notifications about. + /// + /// + /// An IMAP mailbox filter that specifies a list of folders to receive notifications about. + /// + public class Mailboxes : ImapMailboxFilter + { + readonly ImapFolder[] folders; + + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The list of folders to watch for events. + /// + /// is . + /// + /// + /// The list of is empty. + /// -or- + /// The list of contains folders that are not of + /// type . + /// + public Mailboxes (IList folders) : this ("MAILBOXES", folders) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The list of folders to watch for events. + /// + /// is . + /// + /// + /// The list of is empty. + /// -or- + /// The list of contains folders that are not of + /// type . + /// + public Mailboxes (params IMailFolder[] folders) : this ((IList) folders) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The name of the mailbox filter. + /// The list of folders to watch for events. + /// + /// is . + /// + /// + /// The list of is empty. + /// -or- + /// The list of contains folders that are not of + /// type . + /// + internal Mailboxes (string name, IList folders) : base (name) + { + if (folders == null) + throw new ArgumentNullException (nameof (folders)); + + if (folders.Count == 0) + throw new ArgumentException ("Must supply at least one folder.", nameof (folders)); + + this.folders = new ImapFolder[folders.Count]; + for (int i = 0; i < folders.Count; i++) { + if (folders[i] is not ImapFolder folder) + throw new ArgumentException ("All folders must be ImapFolders.", nameof (folders)); + + this.folders[i] = folder; + } + } + + /// + /// Format the IMAP NOTIFY command for this particular IMAP mailbox filter. + /// + /// + /// Formats the IMAP NOTIFY command for this particular IMAP mailbox filter. + /// + /// The IMAP engine. + /// The IMAP command builder. + /// The IMAP command argument builder. + internal override void Format (ImapEngine engine, StringBuilder command, IList args) + { + command.Append (Name); + command.Append (' '); + + // FIXME: should we verify that each ImapFolder belongs to this ImapEngine? + + if (folders.Length == 1) { + command.Append ("%F"); + args.Add (folders[0]); + } else { + command.Append ('('); + + for (int i = 0; i < folders.Length; i++) { + if (i > 0) + command.Append (' '); + command.Append ("%F"); + args.Add (folders[i]); + } + + command.Append (')'); + } + } + } + + /// + /// An IMAP mailbox filter that specifies a list of folder subtrees to get notifications about. + /// + /// + /// The client will receive notifications for each specified folder plus all selectable + /// folders that are subordinate to any of the specified folders. + /// + public class Subtree : Mailboxes + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The list of folders to watch for events. + /// + /// is . + /// + /// + /// The list of is empty. + /// -or- + /// The list of contains folders that are not of + /// type . + /// + public Subtree (IList folders) : base ("SUBTREE", folders) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The list of folders to watch for events. + /// + /// is . + /// + /// + /// The list of is empty. + /// -or- + /// The list of contains folders that are not of + /// type . + /// + public Subtree (params IMailFolder[] folders) : this ((IList) folders) + { + } + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The name of the mailbox filter. + internal ImapMailboxFilter (string name) + { + Name = name; + } + + /// + /// Get the name of the mailbox filter. + /// + /// + /// Gets the name of the mailbox filter. + /// + /// The name. + public string Name { get; private set; } + + /// + /// Format the IMAP NOTIFY command for this particular IMAP mailbox filter. + /// + /// + /// Formats the IMAP NOTIFY command for this particular IMAP mailbox filter. + /// + /// The IMAP engine. + /// The IMAP command builder. + /// The IMAP command argument builder. + internal virtual void Format (ImapEngine engine, StringBuilder command, IList args) + { + command.Append (Name); + } + } + + /// + /// An IMAP notification event. + /// + /// + /// An IMAP notification event. + /// + public class ImapEvent + { + /// + /// An IMAP event notification for expunged messages. + /// + /// + /// If the expunged message or messages are in the selected mailbox, the server notifies the client + /// using (or if + /// the QRESYNC extension has been enabled via + /// or + /// ). + /// If the expunged message or messages are in another mailbox, the + /// and properties will be updated and the appropriate + /// and events will be + /// emitted for the relevant folder. If the QRESYNC + /// extension is enabled, the property will also be updated and + /// the event will be emitted. + /// if a client requests with the + /// mailbox specifier, the meaning of a message index can change at any time, so the client cannot use + /// message indexes in commands anymore. The client MUST use API variants that take or + /// a . The meaning of ** can also change when messages are added or expunged. + /// A client wishing to keep using message indexes can either use the + /// mailbox specifier or can avoid using the event entirely. + /// + public static readonly ImapEvent MessageExpunge = new ImapEvent ("MessageExpunge", true); + + /// + /// An IMAP event notification for message flag changes. + /// + /// + /// If the notification arrives for a message located in the currently selected + /// folder, then that folder will emit a event as well as a + /// event with an appropriately populated + /// . + /// On the other hand, if the notification arrives for a message that is not + /// located in the currently selected folder, then the events that are emitted will depend on the + /// of the IMAP server. + /// If the server supports the capability (or the + /// capability and the client has enabled it via + /// ), then the + /// event will be emitted as well as the + /// event (if the latter has changed). If the number of + /// seen messages has changed, then the event may also be emitted. + /// If the server does not support either the capability nor + /// the capability and the client has not enabled the later capability + /// via , then the server may choose + /// only to notify the client of changes by emitting the + /// event. + /// + public static readonly ImapEvent FlagChange = new ImapEvent ("FlagChange", true); + + /// + /// An IMAP event notification for message annotation changes. + /// + /// + /// If the notification arrives for a message located in the currently selected + /// folder, then that folder will emit a event as well as a + /// event with an appropriately populated + /// . + /// On the other hand, if the notification arrives for a message that is not + /// located in the currently selected folder, then the events that are emitted will depend on the + /// of the IMAP server. + /// If the server supports the capability (or the + /// capability and the client has enabled it via + /// ), then the + /// event will be emitted as well as the + /// event (if the latter has changed). If the number of + /// seen messages has changed, then the event may also be emitted. + /// If the server does not support either the capability nor + /// the capability and the client has not enabled the later capability + /// via , then the server may choose + /// only to notify the client of changes by emitting the + /// event. + /// + public static readonly ImapEvent AnnotationChange = new ImapEvent ("AnnotationChange", true); + + /// + /// AN IMAP event notification for folders that have been created, deleted, or renamed. + /// + /// + /// These notifications are sent if an affected mailbox name was created, deleted, or renamed. + /// As these notifications are received by the client, the appropriate will be emitted: + /// , , or + /// , respectively. + /// If the server supports , granting or revocation of the + /// right to the current user on the affected folder will also be + /// considered folder creation or deletion, respectively. If a folder is created or deleted, the folder itself + /// and its direct parent (whether it is an existing folder or not) are considered to be affected. + /// + public static readonly ImapEvent MailboxName = new ImapEvent ("MailboxName", false); + + /// + /// An IMAP event notification for folders who have had their subscription status changed. + /// + /// + /// This event requests that the server notifies the client of any subscription changes, + /// causing the or + /// events to be emitted accordingly on the affected . + /// + public static readonly ImapEvent SubscriptionChange = new ImapEvent ("SubscriptionChange", false); + + /// + /// An IMAP event notification for changes to folder metadata. + /// + /// + /// Support for this event type is OPTIONAL unless is supported + /// by the server, in which case support for this event type is REQUIRED. + /// If the server does support this event, then the event + /// will be emitted whenever metadata changes for any folder included in the . + /// + public static readonly ImapEvent MailboxMetadataChange = new ImapEvent ("MailboxMetadataChange", false); + + /// + /// An IMAP event notification for changes to server metadata. + /// + /// + /// Support for this event type is OPTIONAL unless is supported + /// by the server, in which case support for this event type is REQUIRED. + /// If the server does support this event, then the event + /// will be emitted whenever metadata changes. + /// + public static readonly ImapEvent ServerMetadataChange = new ImapEvent ("ServerMetadataChange", false); + + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The name of the IMAP event. + /// if the event is a message event; otherwise, . + internal ImapEvent (string name, bool isMessageEvent) + { + IsMessageEvent = isMessageEvent; + Name = name; + } + + /// + /// Get whether or not this is a message event. + /// + /// + /// Gets whether or not this is a message event. + /// + /// if is message event; otherwise, . + internal bool IsMessageEvent { + get; private set; + } + + /// + /// Get the name of the IMAP event. + /// + /// + /// Gets the name of the IMAP event. + /// + /// The name of the IMAP event. + public string Name { + get; private set; + } + + /// + /// Format the IMAP NOTIFY command for this particular IMAP mailbox filter. + /// + /// + /// Formats the IMAP NOTIFY command for this particular IMAP mailbox filter. + /// + /// The IMAP engine. + /// The IMAP command builder. + /// The IMAP command argument builder. + /// if the event is being registered for a + /// or + /// mailbox filter. + internal virtual void Format (ImapEngine engine, StringBuilder command, IList args, bool isSelectedFilter) + { + command.Append (Name); + } + + /// + /// An IMAP event notification for new or appended messages. + /// + /// + /// An IMAP event notification for new or appended messages. + /// If the new or appended message is in the selected folder, the folder will emit the + /// event, followed by a + /// event containing the information requested by the client. + /// These events will not be emitted for any message created by the client on this particular folder + /// as a result of, for example, a call to + /// + /// or . + /// + public class MessageNew : ImapEvent + { + readonly IFetchRequest request; + + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The fetch request to use when new messages arrive. + /// + /// is . + /// + public MessageNew (IFetchRequest request) : base ("MessageNew", true) + { + if (request == null) + throw new ArgumentNullException (nameof (request)); + + this.request = request; + } + + + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The message summary items to automatically retrieve for new messages. + public MessageNew (MessageSummaryItems items = MessageSummaryItems.None) : this (new FetchRequest (items)) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The message summary items to automatically retrieve for new messages. + /// Additional message headers to retrieve for new messages. + /// + /// is . + /// + /// + /// One or more of the specified is invalid. + /// + public MessageNew (MessageSummaryItems items, IEnumerable headers) : this (new FetchRequest (items, headers)) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The message summary items to automatically retrieve for new messages. + /// Additional message headers to retrieve for new messages. + /// + /// is . + /// + /// + /// One or more of the specified is invalid. + /// + public MessageNew (MessageSummaryItems items, IEnumerable headers) : this (new FetchRequest (items, headers)) + { + } + + /// + /// Format the IMAP NOTIFY command for this particular IMAP mailbox filter. + /// + /// + /// Formats the IMAP NOTIFY command for this particular IMAP mailbox filter. + /// + /// The IMAP engine. + /// The IMAP command builder. + /// The IMAP command argument builder. + /// if the event is being registered for a + /// or + /// mailbox filter. + internal override void Format (ImapEngine engine, StringBuilder command, IList args, bool isSelectedFilter) + { + command.Append (Name); + + if (ImapFolder.IsEmptyFetchRequest (request)) + return; + + if (!isSelectedFilter) + throw new InvalidOperationException ("The MessageNew event cannot have any parameters for mailbox filters other than SELECTED and SELECTED-DELAYED."); + + command.Append (' '); + command.Append (ImapFolder.FormatSummaryItems (engine, request, out _, isNotify: true)); + } + } + } +} diff --git a/MailKit/Net/Imap/ImapFolder.cs b/MailKit/Net/Imap/ImapFolder.cs index 1c11075037..498b649f49 100644 --- a/MailKit/Net/Imap/ImapFolder.cs +++ b/MailKit/Net/Imap/ImapFolder.cs @@ -1,9 +1,9 @@ -// +// // ImapFolder.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -25,21 +25,23 @@ // using System; -using System.IO; -using System.Linq; using System.Text; using System.Threading; -using System.Diagnostics; using System.Globalization; using System.Threading.Tasks; using System.Collections.Generic; -using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; using MimeKit; -using MimeKit.IO; -using MimeKit.Utils; + using MailKit.Search; +#if NET5_0_OR_GREATER +using IReadOnlySetOfStrings = System.Collections.Generic.IReadOnlySet; +#else +using IReadOnlySetOfStrings = System.Collections.Generic.ISet; +#endif + namespace MailKit.Net.Imap { /// /// An IMAP folder. @@ -48,13 +50,16 @@ namespace MailKit.Net.Imap { /// An IMAP folder. /// /// - /// + /// /// /// - /// + /// /// - public class ImapFolder : MailFolder + public partial class ImapFolder : MailFolder, IImapFolder { + bool supportsModSeq; + bool countChanged; + /// /// Initializes a new instance of the class. /// @@ -67,23 +72,16 @@ public class ImapFolder : MailFolder /// /// The constructor arguments. /// - /// is null. + /// is . /// public ImapFolder (ImapFolderConstructorArgs args) + : base (args?.FullName ?? string.Empty, args?.DirectorySeparator ?? '.', args?.Attributes ?? FolderAttributes.None) { if (args == null) throw new ArgumentNullException (nameof (args)); - DirectorySeparator = args.DirectorySeparator; EncodedName = args.EncodedName; - Attributes = args.Attributes; - FullName = args.FullName; Engine = args.Engine; - Name = args.Name; - - Engine.Disconnected += (sender, e) => { - Access = FolderAccess.None; - }; } /// @@ -105,7 +103,7 @@ internal ImapEngine Engine { /// /// The encoded name. internal string EncodedName { - get; private set; + get; set; } /// @@ -121,6 +119,41 @@ public override object SyncRoot { get { return Engine; } } + /// + /// Get the threading algorithms supported by the folder. + /// + /// + /// Get the threading algorithms supported by the folder. + /// + /// The supported threading algorithms. + public override HashSet ThreadingAlgorithms { + get { return Engine.ThreadingAlgorithms; } + } + + /// + /// Determine whether or not an supports a feature. + /// + /// + /// Determines whether or not an supports a feature. + /// + /// The desired feature. + /// if the feature is supported; otherwise, . + public override bool Supports (FolderFeature feature) + { + switch (feature) { + case FolderFeature.AccessRights: return (Engine.Capabilities & ImapCapabilities.Acl) != 0; + case FolderFeature.Annotations: return AnnotationAccess != AnnotationAccess.None; + case FolderFeature.Metadata: return (Engine.Capabilities & ImapCapabilities.Metadata) != 0; + case FolderFeature.ModSequences: return supportsModSeq; + case FolderFeature.QuickResync: return Engine.QResyncEnabled; + case FolderFeature.Quotas: return (Engine.Capabilities & ImapCapabilities.Quota) != 0; + case FolderFeature.Sorting: return (Engine.Capabilities & ImapCapabilities.Sort) != 0; + case FolderFeature.Threading: return (Engine.Capabilities & ImapCapabilities.Thread) != 0; + case FolderFeature.UTF8: return Engine.UTF8Enabled; + default: return false; + } + } + void CheckState (bool open, bool rw) { if (Engine.IsDisposed) @@ -140,6 +173,42 @@ void CheckState (bool open, bool rw) } } + void CheckAllowIndexes () + { + // Indexes ("Message Sequence Numbers" or MSNs in the RFCs) and * are not stable while MessageNew/MessageExpunge is registered for SELECTED and therefore should not be used + // https://tools.ietf.org/html/rfc5465#section-5.2 + if (Engine.NotifySelectedNewExpunge) + throw new InvalidOperationException ("Indexes and '*' cannot be used while MessageNew/MessageExpunge is registered with NOTIFY for SELECTED."); + } + + void CheckValidDestination (IMailFolder destination) + { + if (destination == null) + throw new ArgumentNullException (nameof (destination)); + + if (destination is not ImapFolder target || (target.Engine != Engine)) + throw new ArgumentException ("The destination folder does not belong to this ImapClient.", nameof (destination)); + } + + internal void Reset () + { + // basic state + ((HashSet) PermanentKeywords).Clear (); + ((HashSet) AcceptedKeywords).Clear (); + PermanentFlags = MessageFlags.None; + AcceptedFlags = MessageFlags.None; + Access = FolderAccess.None; + + // annotate state + AnnotationAccess = AnnotationAccess.None; + AnnotationScopes = AnnotationScope.None; + MaxAnnotationSize = 0; + + // condstore state + supportsModSeq = false; + HighestModSeq = 0; + } + /// /// Notifies the folder that a parent folder has been renamed. /// @@ -150,11 +219,11 @@ protected override void OnParentFolderRenamed () { var oldEncodedName = EncodedName; - FullName = ParentFolder.FullName + DirectorySeparator + Name; + FullName = ParentFolder!.FullName + DirectorySeparator + Name; EncodedName = Engine.EncodeMailboxName (FullName); Engine.FolderCache.Remove (oldEncodedName); Engine.FolderCache[EncodedName] = this; - Access = FolderAccess.None; + Reset (); if (Engine.Selected == this) { Engine.State = ImapEngineState.Authenticated; @@ -163,23 +232,24 @@ protected override void OnParentFolderRenamed () } } - void ProcessResponseCodes (ImapCommand ic, IMailFolder folder) + void ProcessResponseCodes (ImapCommand ic, IMailFolder? folder, bool throwNotFound = true) { bool tryCreate = false; foreach (var code in ic.RespCodes) { switch (code.Type) { - case ImapResponseCodeType.Alert: - Engine.OnAlert (code.Message); - break; case ImapResponseCodeType.PermanentFlags: - PermanentFlags = ((PermanentFlagsResponseCode) code).Flags; + var permanent = (PermanentFlagsResponseCode) code; + PermanentKeywords = permanent.Keywords; + PermanentFlags = permanent.Flags; break; case ImapResponseCodeType.ReadOnly: - Access = FolderAccess.ReadOnly; + if (code.IsTagged) + Access = FolderAccess.ReadOnly; break; case ImapResponseCodeType.ReadWrite: - Access = FolderAccess.ReadWrite; + if (code.IsTagged) + Access = FolderAccess.ReadWrite; break; case ImapResponseCodeType.TryCreate: tryCreate = true; @@ -188,23 +258,43 @@ void ProcessResponseCodes (ImapCommand ic, IMailFolder folder) UidNext = ((UidNextResponseCode) code).Uid; break; case ImapResponseCodeType.UidValidity: - UidValidity = ((UidValidityResponseCode) code).UidValidity; + var uidValidity = ((UidValidityResponseCode) code).UidValidity; + if (IsOpen) + UpdateUidValidity (uidValidity); + else + UidValidity = uidValidity; break; case ImapResponseCodeType.Unseen: FirstUnread = ((UnseenResponseCode) code).Index; break; case ImapResponseCodeType.HighestModSeq: - HighestModSeq = ((HighestModSeqResponseCode) code).HighestModSeq; - SupportsModSeq = true; + var highestModSeq = ((HighestModSeqResponseCode) code).HighestModSeq; + supportsModSeq = true; + if (IsOpen) + UpdateHighestModSeq (highestModSeq); + else + HighestModSeq = highestModSeq; break; case ImapResponseCodeType.NoModSeq: - SupportsModSeq = false; + supportsModSeq = false; HighestModSeq = 0; break; + case ImapResponseCodeType.MailboxId: + // Note: an untagged MAILBOX resp-code is returned on SELECT/EXAMINE while + // a *tagged* MAILBOXID resp-code is returned on CREATE. + if (!code.IsTagged) + Id = ((MailboxIdResponseCode) code).MailboxId; + break; + case ImapResponseCodeType.Annotations: + var annotations = (AnnotationsResponseCode) code; + AnnotationAccess = annotations.Access; + AnnotationScopes = annotations.Scopes; + MaxAnnotationSize = annotations.MaxSize; + break; } } - if (tryCreate && folder != null) + if (tryCreate && throwNotFound && folder != null) throw new FolderNotFoundException (folder.FullName); } @@ -216,7 +306,7 @@ void ProcessResponseCodes (ImapCommand ic, IMailFolder folder) /// /// Gets a value indicating whether the folder is currently open. /// - /// true if the folder is currently open; otherwise, false. + /// if the folder is currently open; otherwise, . public override bool IsOpen { get { return Engine.Selected == this; } } @@ -226,13 +316,132 @@ static string SelectOrExamine (FolderAccess access) return access == FolderAccess.ReadOnly ? "EXAMINE" : "SELECT"; } - static void QResyncFetch (ImapEngine engine, ImapCommand ic, int index) + static Task UntaggedQResyncFetchHandler (ImapEngine engine, ImapCommand ic, int index, bool doAsync) + { + var folder = ic.Folder!; + + if (doAsync) + return folder.OnUntaggedFetchResponseAsync (engine, index, ic.CancellationToken); + + folder.OnUntaggedFetchResponse (engine, index, ic.CancellationToken); + + return Task.CompletedTask; + } + + ImapCommand QueueOpenCommand (FolderAccess access, uint uidValidity, ulong highestModSeq, IList uids, CancellationToken cancellationToken) + { + if (access != FolderAccess.ReadOnly && access != FolderAccess.ReadWrite) + throw new ArgumentOutOfRangeException (nameof (access)); + + if (uids == null) + throw new ArgumentNullException (nameof (uids)); + + CheckState (false, false); + + if ((Engine.Capabilities & ImapCapabilities.QuickResync) == 0) + throw new NotSupportedException ("The IMAP server does not support the QRESYNC extension."); + + if (!Supports (FolderFeature.QuickResync)) + throw new InvalidOperationException ("The QRESYNC extension has not been enabled."); + + string qresync; + + if ((Engine.Capabilities & ImapCapabilities.Annotate) != 0 && Engine.QuirksMode != ImapQuirksMode.iCloud) + qresync = string.Format (CultureInfo.InvariantCulture, "(ANNOTATE QRESYNC ({0} {1}", uidValidity, highestModSeq); + else + qresync = string.Format (CultureInfo.InvariantCulture, "(QRESYNC ({0} {1}", uidValidity, highestModSeq); + + if (uids.Count > 0) { + var set = UniqueIdSet.ToString (uids); + qresync += " " + set; + } + + qresync += "))"; + + var command = string.Format ("{0} %F {1}\r\n", SelectOrExamine (access), qresync); + var ic = new ImapCommand (Engine, cancellationToken, this, command, this); + ic.RegisterUntaggedHandler ("FETCH", UntaggedQResyncFetchHandler); + + Engine.QueueCommand (ic); + + return ic; + } + + void ProcessOpenResponse (ImapCommand ic, FolderAccess access) + { + ProcessResponseCodes (ic, this); + + ic.ThrowIfNotOk (access == FolderAccess.ReadOnly ? "EXAMINE" : "SELECT"); + } + + FolderAccess Open () + { + if (Engine.Selected != null && Engine.Selected != this) { + var folder = Engine.Selected; + + folder.Reset (); + + folder.OnClosed (); + } + + Engine.State = ImapEngineState.Selected; + Engine.Selected = this; + + OnOpened (); + + return Access; + } + + FolderAccess Open (ImapCommand ic, FolderAccess access) + { + Reset (); + + if (access == FolderAccess.ReadWrite) { + // Note: if the server does not respond with a PERMANENTFLAGS response, + // then we need to assume all flags are permanent. + PermanentFlags = SettableFlags | MessageFlags.UserDefined; + } else { + PermanentFlags = MessageFlags.None; + } + + try { + Engine.Run (ic); + + ProcessOpenResponse (ic, access); + } catch { + PermanentFlags = MessageFlags.None; + throw; + } + + return Open (); + } + + async Task OpenAsync (ImapCommand ic, FolderAccess access) { - ic.Folder.OnFetch (engine, index, ic.CancellationToken); + Reset (); + + if (access == FolderAccess.ReadWrite) { + // Note: if the server does not respond with a PERMANENTFLAGS response, + // then we need to assume all flags are permanent. + PermanentFlags = SettableFlags | MessageFlags.UserDefined; + } else { + PermanentFlags = MessageFlags.None; + } + + try { + await Engine.RunAsync (ic).ConfigureAwait (false); + + ProcessOpenResponse (ic, access); + } catch { + PermanentFlags = MessageFlags.None; + throw; + } + + return Open (); } /// - /// Opens the folder using the requested folder access. + /// Open the folder using the requested folder access. /// /// /// This variant of the @@ -281,85 +490,29 @@ static void QResyncFetch (ImapEngine engine, ImapCommand ic, int index) /// /// The server replied with a NO or BAD response. /// - public override FolderAccess Open (FolderAccess access, uint uidValidity, ulong highestModSeq, IList uids, CancellationToken cancellationToken = default (CancellationToken)) + public override FolderAccess Open (FolderAccess access, uint uidValidity, ulong highestModSeq, IList uids, CancellationToken cancellationToken = default) { - var set = ImapUtils.FormatUidSet (uids); - - if (access != FolderAccess.ReadOnly && access != FolderAccess.ReadWrite) - throw new ArgumentOutOfRangeException (nameof (access)); - - CheckState (false, false); - - if (IsOpen && Access == access) - return access; - - if ((Engine.Capabilities & ImapCapabilities.QuickResync) == 0) - throw new NotSupportedException ("The IMAP server does not support the QRESYNC extension."); - - if (!Engine.QResyncEnabled) - throw new InvalidOperationException ("The QRESYNC extension has not been enabled."); - - var qresync = string.Format ("(QRESYNC ({0} {1}", uidValidity, highestModSeq); - - if (uids.Count > 0) - qresync += " " + set; - - qresync += "))"; - - var command = string.Format ("{0} %F {1}\r\n", SelectOrExamine (access), qresync); - var ic = new ImapCommand (Engine, cancellationToken, this, command, this); - ic.RegisterUntaggedHandler ("FETCH", QResyncFetch); - - if (access == FolderAccess.ReadWrite) { - // Note: if the server does not respond with a PERMANENTFLAGS response, - // then we need to assume all flags are permanent. - PermanentFlags = SettableFlags | MessageFlags.UserDefined; - } else { - PermanentFlags = MessageFlags.None; - } - - try { - Engine.QueueCommand (ic); - Engine.Wait (ic); - - ProcessResponseCodes (ic, this); - - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create (access == FolderAccess.ReadOnly ? "EXAMINE" : "SELECT", ic); - } catch { - PermanentFlags = MessageFlags.None; - throw; - } - - if (Engine.Selected != null && Engine.Selected != this) { - var folder = Engine.Selected; - - folder.PermanentFlags = MessageFlags.None; - folder.AcceptedFlags = MessageFlags.None; - folder.Access = FolderAccess.None; - - folder.OnClosed (); - } - - if (ic.Bye) - return FolderAccess.None; - - Engine.State = ImapEngineState.Selected; - Engine.Selected = this; - - OnOpened (); + var ic = QueueOpenCommand (access, uidValidity, highestModSeq, uids, cancellationToken); - return Access; + return Open (ic, access); } /// - /// Opens the folder using the requested folder access. + /// Asynchronously open the folder using the requested folder access. /// /// - /// Opens the folder using the requested folder access. + /// This variant of the + /// method is meant for quick resynchronization of the folder. Before calling this method, + /// the method MUST be called. + /// You should also make sure to add listeners to the and + /// events to get notifications of changes since + /// the last time the folder was opened. /// /// The state of the folder. /// The requested folder access. + /// The last known value. + /// The last known value. + /// The last known list of unique message identifiers. /// The cancellation token. /// /// is not a valid value. @@ -376,6 +529,12 @@ static void QResyncFetch (ImapEngine engine, ImapCommand ic, int index) /// /// The does not exist. /// + /// + /// The QRESYNC feature has not been enabled. + /// + /// + /// The IMAP server does not support the QRESYNC extension. + /// /// /// The operation was canceled via the cancellation token. /// @@ -388,70 +547,50 @@ static void QResyncFetch (ImapEngine engine, ImapCommand ic, int index) /// /// The server replied with a NO or BAD response. /// - public override FolderAccess Open (FolderAccess access, CancellationToken cancellationToken = default (CancellationToken)) + public override Task OpenAsync (FolderAccess access, uint uidValidity, ulong highestModSeq, IList uids, CancellationToken cancellationToken = default) + { + var ic = QueueOpenCommand (access, uidValidity, highestModSeq, uids, cancellationToken); + + return OpenAsync (ic, access); + } + + ImapCommand QueueOpenCommand (FolderAccess access, CancellationToken cancellationToken) { if (access != FolderAccess.ReadOnly && access != FolderAccess.ReadWrite) throw new ArgumentOutOfRangeException (nameof (access)); CheckState (false, false); - if (IsOpen && Access == access) - return access; - - var condstore = (Engine.Capabilities & ImapCapabilities.CondStore) != 0 ? " (CONDSTORE)" : string.Empty; - var command = string.Format ("{0} %F{1}\r\n", SelectOrExamine (access), condstore); - var ic = new ImapCommand (Engine, cancellationToken, this, command, this); - - if (access == FolderAccess.ReadWrite) { - // Note: if the server does not respond with a PERMANENTFLAGS response, - // then we need to assume all flags are permanent. - PermanentFlags = SettableFlags | MessageFlags.UserDefined; - } else { - PermanentFlags = MessageFlags.None; - } - - try { - Engine.QueueCommand (ic); - Engine.Wait (ic); - - ProcessResponseCodes (ic, this); - - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create (access == FolderAccess.ReadOnly ? "EXAMINE" : "SELECT", ic); - } catch { - PermanentFlags = MessageFlags.None; - throw; - } - - if (Engine.Selected != null && Engine.Selected != this) { - var folder = Engine.Selected; + var @params = string.Empty; - folder.PermanentFlags = MessageFlags.None; - folder.AcceptedFlags = MessageFlags.None; - folder.Access = FolderAccess.None; - - folder.OnClosed (); - } + if ((Engine.Capabilities & ImapCapabilities.CondStore) != 0) + @params += "CONDSTORE"; + if ((Engine.Capabilities & ImapCapabilities.Annotate) != 0 && Engine.QuirksMode != ImapQuirksMode.iCloud) + @params += " ANNOTATE"; - if (ic.Bye) - return FolderAccess.None; + if (@params.Length > 0) + @params = " (" + @params.TrimStart () + ")"; - Engine.State = ImapEngineState.Selected; - Engine.Selected = this; + var command = string.Format ("{0} %F{1}\r\n", SelectOrExamine (access), @params); + var ic = new ImapCommand (Engine, cancellationToken, this, command, this); - OnOpened (); + Engine.QueueCommand (ic); - return Access; + return ic; } /// - /// Closes the folder, optionally expunging the messages marked for deletion. + /// Open the folder using the requested folder access. /// /// - /// Closes the folder, optionally expunging the messages marked for deletion. + /// Opens the folder using the requested folder access. /// - /// If set to true, expunge. + /// The state of the folder. + /// The requested folder access. /// The cancellation token. + /// + /// is not a valid value. + /// /// /// The has been disposed. /// @@ -461,8 +600,8 @@ static void QResyncFetch (ImapEngine engine, ImapCommand ic, int index) /// /// The is not authenticated. /// - /// - /// The is not currently open. + /// + /// The does not exist. /// /// /// The operation was canceled via the cancellation token. @@ -476,30 +615,83 @@ static void QResyncFetch (ImapEngine engine, ImapCommand ic, int index) /// /// The server replied with a NO or BAD response. /// - public override void Close (bool expunge = false, CancellationToken cancellationToken = default (CancellationToken)) + public override FolderAccess Open (FolderAccess access, CancellationToken cancellationToken = default) { - CheckState (true, expunge); + var ic = QueueOpenCommand (access, cancellationToken); - ImapCommand ic; + return Open (ic, access); + } - if (expunge) { - ic = Engine.QueueCommand (cancellationToken, this, "CLOSE\r\n"); - } else if ((Engine.Capabilities & ImapCapabilities.Unselect) != 0) { - ic = Engine.QueueCommand (cancellationToken, this, "UNSELECT\r\n"); - } else { - ic = null; - } + /// + /// Asynchronously open the folder using the requested folder access. + /// + /// + /// Opens the folder using the requested folder access. + /// + /// The state of the folder. + /// The requested folder access. + /// The cancellation token. + /// + /// is not a valid value. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task OpenAsync (FolderAccess access, CancellationToken cancellationToken = default) + { + var ic = QueueOpenCommand (access, cancellationToken); - if (ic != null) { - Engine.Wait (ic); + return OpenAsync (ic, access); + } + + ImapCommand? QueueCloseCommand (bool expunge, CancellationToken cancellationToken) + { + CheckState (true, expunge); - ProcessResponseCodes (ic, null); + ImapCommand? ic; - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create (expunge ? "CLOSE" : "UNSELECT", ic); + if (expunge) { + ic = Engine.QueueCommand (cancellationToken, this, "CLOSE\r\n"); + } else if ((Engine.Capabilities & ImapCapabilities.Unselect) != 0) { + ic = Engine.QueueCommand (cancellationToken, this, "UNSELECT\r\n"); + } else { + ic = null; } - Access = FolderAccess.None; + return ic; + } + + void ProcessCloseResponse (ImapCommand ic, bool expunge) + { + ProcessResponseCodes (ic, null); + + ic.ThrowIfNotOk (expunge ? "CLOSE" : "UNSELECT"); + } + + void Close () + { + Reset (); if (Engine.Selected == this) { Engine.State = ImapEngineState.Authenticated; @@ -509,21 +701,59 @@ static void QResyncFetch (ImapEngine engine, ImapCommand ic, int index) } /// - /// Creates a new subfolder with the given name. + /// Close the folder, optionally expunging the messages marked for deletion. /// /// - /// Creates a new subfolder with the given name. + /// Closes the folder, optionally expunging the messages marked for deletion. /// - /// The created folder. - /// The name of the folder to create. - /// true if the folder will be used to contain messages; otherwise false. + /// If set to , expunge. /// The cancellation token. - /// - /// is null. + /// + /// The has been disposed. /// - /// - /// is empty. + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. /// + /// + /// The server replied with a NO or BAD response. + /// + public override void Close (bool expunge = false, CancellationToken cancellationToken = default) + { + var ic = QueueCloseCommand (expunge, cancellationToken); + + if (ic != null) { + Engine.Run (ic); + + ProcessCloseResponse (ic, expunge); + } + + Close (); + } + + /// + /// Asynchronously close the folder, optionally expunging the messages marked for deletion. + /// + /// + /// Closes the folder, optionally expunging the messages marked for deletion. + /// + /// An asynchronous task context. + /// If set to , expunge. + /// The cancellation token. /// /// The has been disposed. /// @@ -533,8 +763,8 @@ static void QResyncFetch (ImapEngine engine, ImapCommand ic, int index) /// /// The is not authenticated. /// - /// - /// The is nil, and thus child folders cannot be created. + /// + /// The is not currently open. /// /// /// The operation was canceled via the cancellation token. @@ -548,12 +778,58 @@ static void QResyncFetch (ImapEngine engine, ImapCommand ic, int index) /// /// The server replied with a NO or BAD response. /// - public override IMailFolder Create (string name, bool isMessageFolder, CancellationToken cancellationToken = default (CancellationToken)) + public override async Task CloseAsync (bool expunge = false, CancellationToken cancellationToken = default) + { + var ic = QueueCloseCommand (expunge, cancellationToken); + + if (ic != null) { + await Engine.RunAsync (ic).ConfigureAwait (false); + + ProcessCloseResponse (ic, expunge); + } + + Close (); + } + + ImapCommand QueueGetCreatedFolderCommand (string encodedName, CancellationToken cancellationToken) + { + var ic = new ImapCommand (Engine, cancellationToken, null, "LIST \"\" %S\r\n", encodedName); + ic.RegisterUntaggedHandler ("LIST", ImapUtils.UntaggedListHandler); + ic.UserData = new List (); + + Engine.QueueCommand (ic); + + return ic; + } + + IMailFolder? ProcessGetCreatedFolderResponse (ImapCommand ic, string encodedName, string? id, bool specialUse) + { + var list = (List) ic.UserData!; + ImapFolder? folder; + + ProcessResponseCodes (ic, null); + + ic.ThrowIfNotOk ("LIST"); + + if ((folder = ImapEngine.GetFolder (list, encodedName)) != null) { + folder.ParentFolder = this; + folder.Id = id; + + if (specialUse) + Engine.AssignSpecialFolder (folder); + + Engine.OnFolderCreated (folder); + } + + return folder; + } + + ImapCommand QueueCreateCommand (string name, bool isMessageFolder, CancellationToken cancellationToken, out string encodedName) { if (name == null) throw new ArgumentNullException (nameof (name)); - if (!Engine.IsValidMailboxName (name, DirectorySeparator)) + if (!ImapEngine.IsValidMailboxName (name, DirectorySeparator)) throw new ArgumentException ("The name is not a legal folder name.", nameof (name)); CheckState (false, false); @@ -562,58 +838,68 @@ static void QResyncFetch (ImapEngine engine, ImapCommand ic, int index) throw new InvalidOperationException ("Cannot create child folders."); var fullName = !string.IsNullOrEmpty (FullName) ? FullName + DirectorySeparator + name : name; - var encodedName = Engine.EncodeMailboxName (fullName); - var list = new List (); + encodedName = Engine.EncodeMailboxName (fullName); var createName = encodedName; - ImapFolder folder; - if (!isMessageFolder) + if (!isMessageFolder && Engine.QuirksMode != ImapQuirksMode.GMail) createName += DirectorySeparator; - var ic = Engine.QueueCommand (cancellationToken, null, "CREATE %S\r\n", createName); - - Engine.Wait (ic); + return Engine.QueueCommand (cancellationToken, null, "CREATE %S\r\n", createName); + } + MailboxIdResponseCode? ProcessCreateResponse (ImapCommand ic) + { ProcessResponseCodes (ic, null); - if (ic.Response != ImapCommandResponse.Ok) + if (ic.Response != ImapCommandResponse.Ok && ic.GetResponseCode (ImapResponseCodeType.AlreadyExists) == null) throw ImapCommandException.Create ("CREATE", ic); - ic = new ImapCommand (Engine, cancellationToken, null, "LIST \"\" %S\r\n", encodedName); - ic.RegisterUntaggedHandler ("LIST", ImapUtils.ParseFolderList); - ic.UserData = list; + return ic.GetResponseCode (ImapResponseCodeType.MailboxId) as MailboxIdResponseCode; + } - Engine.QueueCommand (ic); - Engine.Wait (ic); + IMailFolder? Create (ImapCommand ic, string encodedName, bool specialUse, CancellationToken cancellationToken) + { + Engine.Run (ic); - ProcessResponseCodes (ic, null); + var mailboxIdResponseCode = ProcessCreateResponse (ic); + var id = mailboxIdResponseCode?.MailboxId; - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("LIST", ic); + ic = QueueGetCreatedFolderCommand (encodedName, cancellationToken); - if ((folder = list.FirstOrDefault ()) != null) - folder.ParentFolder = this; + Engine.Run (ic); - return folder; + return ProcessGetCreatedFolderResponse (ic, encodedName, id, specialUse); + } + + async Task CreateAsync (ImapCommand ic, string encodedName, bool specialUse, CancellationToken cancellationToken) + { + await Engine.RunAsync (ic).ConfigureAwait (false); + + var mailboxIdResponseCode = ProcessCreateResponse (ic); + var id = mailboxIdResponseCode?.MailboxId; + + ic = QueueGetCreatedFolderCommand (encodedName, cancellationToken); + + await Engine.RunAsync (ic).ConfigureAwait (false); + + return ProcessGetCreatedFolderResponse (ic, encodedName, id, specialUse); } /// - /// Creates a new subfolder with the given name. + /// Create a new subfolder with the given name. /// /// /// Creates a new subfolder with the given name. /// /// The created folder. /// The name of the folder to create. - /// A list of special uses for the folder being created. + /// if the folder will be used to contain messages; otherwise, . /// The cancellation token. /// - /// is null. - /// -or- - /// is null. + /// is . /// /// - /// is empty. + /// is empty or invalid. /// /// /// The has been disposed. @@ -627,8 +913,52 @@ static void QResyncFetch (ImapEngine engine, ImapCommand ic, int index) /// /// The is nil, and thus child folders cannot be created. /// - /// - /// The IMAP server does not support the CREATE-SPECIAL-USE extension. + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override IMailFolder? Create (string name, bool isMessageFolder, CancellationToken cancellationToken = default) + { + var ic = QueueCreateCommand (name, isMessageFolder, cancellationToken, out var encodedName); + + return Create (ic, encodedName, false, cancellationToken); + } + + /// + /// Asynchronously create a new subfolder with the given name. + /// + /// + /// Creates a new subfolder with the given name. + /// + /// The created folder. + /// The name of the folder to create. + /// if the folder will be used to contain messages; otherwise, . + /// The cancellation token. + /// + /// is . + /// + /// + /// is empty or invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is nil, and thus child folders cannot be created. /// /// /// The operation was canceled via the cancellation token. @@ -642,12 +972,19 @@ static void QResyncFetch (ImapEngine engine, ImapCommand ic, int index) /// /// The server replied with a NO or BAD response. /// - public override IMailFolder Create (string name, IEnumerable specialUses, CancellationToken cancellationToken = default (CancellationToken)) + public override Task CreateAsync (string name, bool isMessageFolder, CancellationToken cancellationToken = default) + { + var ic = QueueCreateCommand (name, isMessageFolder, cancellationToken, out var encodedName); + + return CreateAsync (ic, encodedName, false, cancellationToken); + } + + ImapCommand QueueCreateCommand (string name, IEnumerable specialUses, CancellationToken cancellationToken, out string encodedName) { if (name == null) throw new ArgumentNullException (nameof (name)); - if (!Engine.IsValidMailboxName (name, DirectorySeparator)) + if (!ImapEngine.IsValidMailboxName (name, DirectorySeparator)) throw new ArgumentException ("The name is not a legal folder name.", nameof (name)); if (specialUses == null) @@ -662,83 +999,61 @@ static void QResyncFetch (ImapEngine engine, ImapCommand ic, int index) throw new NotSupportedException ("The IMAP server does not support the CREATE-SPECIAL-USE extension."); var uses = new StringBuilder (); + uint used = 0; foreach (var use in specialUses) { + var bit = (uint) (1 << ((int) use)); + + if ((used & bit) != 0) + continue; + + used |= bit; + if (uses.Length > 0) uses.Append (' '); switch (use) { - case SpecialFolder.All: uses.Append ("\\All"); break; - case SpecialFolder.Archive: uses.Append ("\\Archive"); break; - case SpecialFolder.Drafts: uses.Append ("\\Drafts"); break; - case SpecialFolder.Flagged: uses.Append ("\\Flagged"); break; - case SpecialFolder.Junk: uses.Append ("\\Junk"); break; - case SpecialFolder.Sent: uses.Append ("\\Sent"); break; - case SpecialFolder.Trash: uses.Append ("\\Trash"); break; + case SpecialFolder.All: uses.Append ("\\All"); break; + case SpecialFolder.Archive: uses.Append ("\\Archive"); break; + case SpecialFolder.Drafts: uses.Append ("\\Drafts"); break; + case SpecialFolder.Flagged: uses.Append ("\\Flagged"); break; + case SpecialFolder.Important: uses.Append ("\\Important"); break; + case SpecialFolder.Junk: uses.Append ("\\Junk"); break; + case SpecialFolder.Sent: uses.Append ("\\Sent"); break; + case SpecialFolder.Trash: uses.Append ("\\Trash"); break; default: if (uses.Length > 0) uses.Length--; break; } } var fullName = !string.IsNullOrEmpty (FullName) ? FullName + DirectorySeparator + name : name; - var command = string.Format ("CREATE %s (USE ({0}))\r\n", uses); - var encodedName = Engine.EncodeMailboxName (fullName); - var list = new List (); - var createName = encodedName; - ImapFolder folder; - - var ic = Engine.QueueCommand (cancellationToken, null, command, createName); - - Engine.Wait (ic); - - ProcessResponseCodes (ic, null); + encodedName = Engine.EncodeMailboxName (fullName); + string command; - if (ic.Response != ImapCommandResponse.Ok) { - var useAttr = ic.RespCodes.FirstOrDefault (rc => rc.Type == ImapResponseCodeType.UseAttr); - - if (useAttr != null) - throw new ImapCommandException (ic.Response, useAttr.Message); - - throw ImapCommandException.Create ("CREATE", ic); - } - - ic = new ImapCommand (Engine, cancellationToken, null, "LIST \"\" %S\r\n", encodedName); - ic.RegisterUntaggedHandler ("LIST", ImapUtils.ParseFolderList); - ic.UserData = list; - - Engine.QueueCommand (ic); - Engine.Wait (ic); - - ProcessResponseCodes (ic, null); - - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("LIST", ic); - - if ((folder = list.FirstOrDefault ()) != null) - folder.ParentFolder = this; - - Engine.AssignSpecialFolders (new [] { folder }); + if (uses.Length > 0) + command = string.Format ("CREATE %S (USE ({0}))\r\n", uses); + else + command = "CREATE %S\r\n"; - return folder; + return Engine.QueueCommand (cancellationToken, null, command, encodedName); } /// - /// Renames the folder to exist with a new name under a new parent folder. + /// Create a new subfolder with the given name. /// /// - /// Renames the folder to exist with a new name under a new parent folder. + /// Creates a new subfolder with the given name. /// - /// The new parent folder. - /// The new name of the folder. + /// The created folder. + /// The name of the folder to create. + /// A list of special uses for the folder being created. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// - /// does not belong to the . - /// -or- - /// is not a legal folder name. + /// is empty or invalid. /// /// /// The has been disposed. @@ -749,11 +1064,11 @@ static void QResyncFetch (ImapEngine engine, ImapCommand ic, int index) /// /// The is not authenticated. /// - /// - /// The does not exist. - /// /// - /// The folder cannot be renamed (it is either a namespace or the Inbox). + /// The is nil, and thus child folders cannot be created. + /// + /// + /// The IMAP server does not support the CREATE-SPECIAL-USE extension. /// /// /// The operation was canceled via the cancellation token. @@ -767,24 +1082,86 @@ static void QResyncFetch (ImapEngine engine, ImapCommand ic, int index) /// /// The server replied with a NO or BAD response. /// - public override void Rename (IMailFolder parent, string name, CancellationToken cancellationToken = default (CancellationToken)) + public override IMailFolder? Create (string name, IEnumerable specialUses, CancellationToken cancellationToken = default) { - if (parent == null) - throw new ArgumentNullException (nameof (parent)); - - if (!(parent is ImapFolder) || ((ImapFolder) parent).Engine != Engine) - throw new ArgumentException ("The parent folder does not belong to this ImapClient.", nameof (parent)); - - if (name == null) - throw new ArgumentNullException (nameof (name)); - - if (!Engine.IsValidMailboxName (name, DirectorySeparator)) - throw new ArgumentException ("The name is not a legal folder name.", nameof (name)); + var ic = QueueCreateCommand (name, specialUses, cancellationToken, out var encodedName); - if (IsNamespace || (Attributes & FolderAttributes.Inbox) != 0) - throw new InvalidOperationException ("Cannot rename this folder."); + return Create (ic, encodedName, true, cancellationToken); + } - CheckState (false, false); + /// + /// Asynchronously create a new subfolder with the given name. + /// + /// + /// Creates a new subfolder with the given name. + /// + /// The created folder. + /// The name of the folder to create. + /// A list of special uses for the folder being created. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// is empty or invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is nil, and thus child folders cannot be created. + /// + /// + /// The IMAP server does not support the CREATE-SPECIAL-USE extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task CreateAsync (string name, IEnumerable specialUses, CancellationToken cancellationToken = default) + { + var ic = QueueCreateCommand (name, specialUses, cancellationToken, out var encodedName); + + return CreateAsync (ic, encodedName, true, cancellationToken); + } + + ImapCommand QueueRenameCommand (IMailFolder parent, string name, CancellationToken cancellationToken, out string encodedName) + { + if (parent == null) + throw new ArgumentNullException (nameof (parent)); + + if (object.ReferenceEquals (parent, this)) + throw new ArgumentException ("Cannot rename a folder using itself as the new parent folder.", nameof (parent)); + + if (parent is not ImapFolder || ((ImapFolder) parent).Engine != Engine) + throw new ArgumentException ("The parent folder does not belong to this ImapClient.", nameof (parent)); + + if (name == null) + throw new ArgumentNullException (nameof (name)); + + if (!ImapEngine.IsValidMailboxName (name, DirectorySeparator)) + throw new ArgumentException ("The name is not a legal folder name.", nameof (name)); + + if (IsNamespace || (Attributes & FolderAttributes.Inbox) != 0) + throw new InvalidOperationException ("Cannot rename this folder."); + + CheckState (false, false); string newFullName; @@ -793,16 +1170,18 @@ static void QResyncFetch (ImapEngine engine, ImapCommand ic, int index) else newFullName = name; - var encodedName = Engine.EncodeMailboxName (newFullName); - var ic = Engine.QueueCommand (cancellationToken, null, "RENAME %F %S\r\n", this, encodedName); - var oldFullName = FullName; + encodedName = Engine.EncodeMailboxName (newFullName); + + return Engine.QueueCommand (cancellationToken, null, "RENAME %F %S\r\n", this, encodedName); + } - Engine.Wait (ic); + void ProcessRenameResponse (ImapCommand ic, IMailFolder parent, string name, string encodedName) + { + var oldFullName = FullName; ProcessResponseCodes (ic, this); - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("RENAME", ic); + ic.ThrowIfNotOk ("RENAME"); Engine.FolderCache.Remove (EncodedName); Engine.FolderCache[encodedName] = this; @@ -810,10 +1189,11 @@ static void QResyncFetch (ImapEngine engine, ImapCommand ic, int index) ParentFolder = parent; FullName = Engine.DecodeMailboxName (encodedName); - Access = FolderAccess.None; EncodedName = encodedName; Name = name; + Reset (); + if (Engine.Selected == this) { Engine.State = ImapEngineState.Authenticated; Engine.Selected = null; @@ -824,13 +1204,24 @@ static void QResyncFetch (ImapEngine engine, ImapCommand ic, int index) } /// - /// Deletes the folder on the IMAP server. + /// Rename the folder to exist with a new name under a new parent folder. /// /// - /// Deletes the folder on the IMAP server. - /// This method will not delete any child folders. + /// Renames the folder to exist with a new name under a new parent folder. /// + /// The new parent folder. + /// The new name of the folder. /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// does not belong to the . + /// -or- + /// is not a legal folder name. + /// /// /// The has been disposed. /// @@ -840,8 +1231,67 @@ static void QResyncFetch (ImapEngine engine, ImapCommand ic, int index) /// /// The is not authenticated. /// + /// + /// The does not exist. + /// /// - /// The folder cannot be deleted (it is either a namespace or the Inbox). + /// The folder cannot be renamed (it is either a namespace or the Inbox). + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override void Rename (IMailFolder parent, string name, CancellationToken cancellationToken = default) + { + var ic = QueueRenameCommand (parent, name, cancellationToken, out var encodedName); + + Engine.Run (ic); + + ProcessRenameResponse (ic, parent, name, encodedName); + } + + /// + /// Asynchronously rename the folder to exist with a new name under a new parent folder. + /// + /// + /// Renames the folder to exist with a new name under a new parent folder. + /// + /// An awaitable task. + /// The new parent folder. + /// The new name of the folder. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// does not belong to the . + /// -or- + /// is not a legal folder name. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// The folder cannot be renamed (it is either a namespace or the Inbox). /// /// /// The operation was canceled via the cancellation token. @@ -855,23 +1305,32 @@ static void QResyncFetch (ImapEngine engine, ImapCommand ic, int index) /// /// The server replied with a NO or BAD response. /// - public override void Delete (CancellationToken cancellationToken = default (CancellationToken)) + public override async Task RenameAsync (IMailFolder parent, string name, CancellationToken cancellationToken = default) + { + var ic = QueueRenameCommand (parent, name, cancellationToken, out var encodedName); + + await Engine.RunAsync (ic).ConfigureAwait (false); + + ProcessRenameResponse (ic, parent, name, encodedName); + } + + ImapCommand QueueDeleteCommand (CancellationToken cancellationToken) { if (IsNamespace || (Attributes & FolderAttributes.Inbox) != 0) throw new InvalidOperationException ("Cannot delete this folder."); CheckState (false, false); - var ic = Engine.QueueCommand (cancellationToken, null, "DELETE %F\r\n", this); - - Engine.Wait (ic); + return Engine.QueueCommand (cancellationToken, null, "DELETE %F\r\n", this); + } + void ProcessDeleteResponse (ImapCommand ic) + { ProcessResponseCodes (ic, this); - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("DELETE", ic); + ic.ThrowIfNotOk ("DELETE"); - Access = FolderAccess.None; + Reset (); if (Engine.Selected == this) { Engine.State = ImapEngineState.Authenticated; @@ -884,10 +1343,11 @@ static void QResyncFetch (ImapEngine engine, ImapCommand ic, int index) } /// - /// Subscribes the folder. + /// Delete the folder on the IMAP server. /// /// - /// Subscribes the folder. + /// Deletes the folder on the IMAP server. + /// This method will not delete any child folders. /// /// The cancellation token. /// @@ -899,6 +1359,9 @@ static void QResyncFetch (ImapEngine engine, ImapCommand ic, int index) /// /// The is not authenticated. /// + /// + /// The folder cannot be deleted (it is either a namespace or the Inbox). + /// /// /// The operation was canceled via the cancellation token. /// @@ -911,30 +1374,23 @@ static void QResyncFetch (ImapEngine engine, ImapCommand ic, int index) /// /// The server replied with a NO or BAD response. /// - public override void Subscribe (CancellationToken cancellationToken = default (CancellationToken)) + public override void Delete (CancellationToken cancellationToken = default) { - CheckState (false, false); - - var ic = Engine.QueueCommand (cancellationToken, null, "SUBSCRIBE %F\r\n", this); + var ic = QueueDeleteCommand (cancellationToken); - Engine.Wait (ic); + Engine.Run (ic); - ProcessResponseCodes (ic, null); - - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("SUBSCRIBE", ic); - - Attributes |= FolderAttributes.Subscribed; - - OnSubscribed (); + ProcessDeleteResponse (ic); } /// - /// Unsubscribes the folder. + /// Asynchronously delete the folder on the IMAP server. /// /// - /// Unsubscribes the folder. + /// Deletes the folder on the IMAP server. + /// This method will not delete any child folders. /// + /// An awaitable task. /// The cancellation token. /// /// The has been disposed. @@ -945,6 +1401,9 @@ static void QResyncFetch (ImapEngine engine, ImapCommand ic, int index) /// /// The is not authenticated. /// + /// + /// The folder cannot be deleted (it is either a namespace or the Inbox). + /// /// /// The operation was canceled via the cancellation token. /// @@ -957,33 +1416,41 @@ static void QResyncFetch (ImapEngine engine, ImapCommand ic, int index) /// /// The server replied with a NO or BAD response. /// - public override void Unsubscribe (CancellationToken cancellationToken = default (CancellationToken)) + public override async Task DeleteAsync (CancellationToken cancellationToken = default) { - CheckState (false, false); + var ic = QueueDeleteCommand (cancellationToken); - var ic = Engine.QueueCommand (cancellationToken, null, "UNSUBSCRIBE %F\r\n", this); + await Engine.RunAsync (ic).ConfigureAwait (false); + + ProcessDeleteResponse (ic); + } + + ImapCommand QueueSubscribeCommand (CancellationToken cancellationToken) + { + CheckState (false, false); - Engine.Wait (ic); + return Engine.QueueCommand (cancellationToken, null, "SUBSCRIBE %F\r\n", this); + } + void ProcessSubscribeResponse (ImapCommand ic) + { ProcessResponseCodes (ic, null); - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("UNSUBSCRIBE", ic); + ic.ThrowIfNotOk ("SUBSCRIBE"); - Attributes &= ~FolderAttributes.Subscribed; + if ((Attributes & FolderAttributes.Subscribed) == 0) { + Attributes |= FolderAttributes.Subscribed; - OnUnsubscribed (); + OnSubscribed (); + } } /// - /// Gets the subfolders. + /// Subscribe the folder. /// /// - /// Gets the subfolders. + /// Subscribes the folder. /// - /// The subfolders. - /// The status items to pre-populate. - /// If set to true, only subscribed folders will be listed. /// The cancellation token. /// /// The has been disposed. @@ -1006,112 +1473,23 @@ static void QResyncFetch (ImapEngine engine, ImapCommand ic, int index) /// /// The server replied with a NO or BAD response. /// - public override IEnumerable GetSubfolders (StatusItems items, bool subscribedOnly = false, CancellationToken cancellationToken = default (CancellationToken)) + public override void Subscribe (CancellationToken cancellationToken = default) { - CheckState (false, false); - - var pattern = EncodedName.Length > 0 ? EncodedName + DirectorySeparator : string.Empty; - var children = new List (); - var status = items != StatusItems.None; - var list = new List (); - var command = new StringBuilder (); - var lsub = subscribedOnly; - - if (subscribedOnly) { - if ((Engine.Capabilities & ImapCapabilities.ListExtended) != 0) { - command.Append ("LIST (SUBSCRIBED)"); - lsub = false; - } else { - command.Append ("LSUB"); - } - } else { - command.Append ("LIST"); - } - - command.Append (" \"\" %S"); - - if (!lsub) { - if (items != StatusItems.None && (Engine.Capabilities & ImapCapabilities.ListStatus) != 0) { - command.Append (" RETURN ("); - - if ((Engine.Capabilities & ImapCapabilities.ListExtended) != 0) { - if (!subscribedOnly) - command.Append ("SUBSCRIBED "); - command.Append ("CHILDREN "); - } - - command.AppendFormat ("STATUS ({0})", Engine.GetStatusQuery (items)); - command.Append (')'); - status = false; - } else if ((Engine.Capabilities & ImapCapabilities.ListExtended) != 0) { - command.Append (" RETURN ("); - if (!subscribedOnly) - command.Append ("SUBSCRIBED "); - command.Append ("CHILDREN"); - command.Append (')'); - } - } - - command.Append ("\r\n"); - - var ic = new ImapCommand (Engine, cancellationToken, null, command.ToString (), pattern + "%"); - ic.RegisterUntaggedHandler (lsub ? "LSUB" : "LIST", ImapUtils.ParseFolderList); - ic.UserData = list; - - Engine.QueueCommand (ic); - Engine.Wait (ic); - - // Note: Some broken IMAP servers (*cough* SmarterMail 13.0 *cough*) return folders - // that are not children of the folder we requested, so we need to filter those - // folders out of the list that we'll be returning to our caller. - // - // See https://github.com/jstedfast/MailKit/issues/149 for more details. - var prefix = FullName.Length > 0 ? FullName + DirectorySeparator : string.Empty; - prefix = ImapUtils.CanonicalizeMailboxName (prefix, DirectorySeparator); - foreach (var folder in list) { - var canonicalFullName = ImapUtils.CanonicalizeMailboxName (folder.FullName, folder.DirectorySeparator); - var canonicalName = ImapUtils.IsInbox (folder.FullName) ? "INBOX" : folder.Name; - - if (canonicalFullName != prefix + canonicalName) - continue; - - if (lsub) { - // the LSUB command does not send \Subscribed flags so we need to add them ourselves - folder.Attributes |= FolderAttributes.Subscribed; - } - - folder.ParentFolder = this; - children.Add (folder); - } - - ProcessResponseCodes (ic, null); + var ic = QueueSubscribeCommand (cancellationToken); - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create (lsub ? "LSUB" : "LIST", ic); + Engine.Run (ic); - if (status) { - for (int i = 0; i < children.Count; i++) - children[i].Status (items, cancellationToken); - } - - return children; + ProcessSubscribeResponse (ic); } /// - /// Gets the specified subfolder. + /// Asynchronously subscribe the folder. /// /// - /// Gets the specified subfolder. + /// Subscribes the folder. /// - /// The subfolder. - /// The name of the subfolder. + /// An awaitable task. /// The cancellation token. - /// - /// is null. - /// - /// - /// is either an empty string or contains the . - /// /// /// The has been disposed. /// @@ -1127,67 +1505,51 @@ static void QResyncFetch (ImapEngine engine, ImapCommand ic, int index) /// /// An I/O error occurred. /// - /// - /// The requested folder could not be found. - /// /// /// The server's response contained unexpected tokens. /// /// /// The server replied with a NO or BAD response. /// - public override IMailFolder GetSubfolder (string name, CancellationToken cancellationToken = default (CancellationToken)) + public override async Task SubscribeAsync (CancellationToken cancellationToken = default) { - if (name == null) - throw new ArgumentNullException (nameof (name)); + var ic = QueueSubscribeCommand (cancellationToken); - if (!Engine.IsValidMailboxName (name, DirectorySeparator)) - throw new ArgumentException ("The name of the subfolder is invalid.", nameof (name)); + await Engine.RunAsync (ic).ConfigureAwait (false); - CheckState (false, false); - - var fullName = FullName.Length > 0 ? FullName + DirectorySeparator + name : name; - var encodedName = Engine.EncodeMailboxName (fullName); - List list; - ImapFolder subfolder; - - if (Engine.GetCachedFolder (encodedName, out subfolder)) - return subfolder; + ProcessSubscribeResponse (ic); + } - var ic = new ImapCommand (Engine, cancellationToken, null, "LIST \"\" %S\r\n", encodedName); - ic.RegisterUntaggedHandler ("LIST", ImapUtils.ParseFolderList); - ic.UserData = list = new List (); + ImapCommand QueueUnsubscribeCommand (CancellationToken cancellationToken) + { + CheckState (false, false); - Engine.QueueCommand (ic); - Engine.Wait (ic); + return Engine.QueueCommand (cancellationToken, null, "UNSUBSCRIBE %F\r\n", this); + } + void ProcessUnsubscribeResponse (ImapCommand ic) + { ProcessResponseCodes (ic, null); - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("LIST", ic); + ic.ThrowIfNotOk ("UNSUBSCRIBE"); - if (list.Count == 0) - throw new FolderNotFoundException (fullName); + if ((Attributes & FolderAttributes.Subscribed) != 0) { + Attributes &= ~FolderAttributes.Subscribed; - return list[0]; + OnUnsubscribed (); + } } /// - /// Force the server to sync its in-memory state with its disk state. + /// Unsubscribe the folder. /// /// - /// The CHECK command forces the IMAP server to sync its - /// in-memory state with its disk state. - /// For more information about the CHECK command, see - /// rfc350101. + /// Unsubscribes the folder. /// /// The cancellation token. /// /// The has been disposed. /// - /// - /// The is not currently open. - /// /// /// The is not connected. /// @@ -1206,36 +1568,22 @@ static void QResyncFetch (ImapEngine engine, ImapCommand ic, int index) /// /// The server replied with a NO or BAD response. /// - public override void Check (CancellationToken cancellationToken = default (CancellationToken)) + public override void Unsubscribe (CancellationToken cancellationToken = default) { - CheckState (true, false); - - var ic = Engine.QueueCommand (cancellationToken, this, "CHECK\r\n"); + var ic = QueueUnsubscribeCommand (cancellationToken); - Engine.Wait (ic); + Engine.Run (ic); - ProcessResponseCodes (ic, null); - - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("CHECK", ic); + ProcessUnsubscribeResponse (ic); } /// - /// Updates the values of the specified items. + /// Asynchronously unsubscribe the folder. /// /// - /// Updates the values of the specified items. - /// The method - /// MUST NOT be used on a folder that is already in the opened state. Instead, other ways - /// of getting the desired information should be used. - /// For example, a common use for the - /// method is to get the number of unread messages in the folder. When the folder is open, however, it is - /// possible to use the - /// method to query for the list of unread messages. - /// For more information about the STATUS command, see - /// rfc3501. + /// Unsubscribes the folder. /// - /// The items to update. + /// An awaitable task. /// The cancellation token. /// /// The has been disposed. @@ -1246,12 +1594,6 @@ static void QResyncFetch (ImapEngine engine, ImapCommand ic, int index) /// /// The is not authenticated. /// - /// - /// The does not exist. - /// - /// - /// The IMAP server does not support the STATUS command. - /// /// /// The operation was canceled via the cancellation token. /// @@ -1264,54 +1606,143 @@ static void QResyncFetch (ImapEngine engine, ImapCommand ic, int index) /// /// The server replied with a NO or BAD response. /// - public override void Status (StatusItems items, CancellationToken cancellationToken = default (CancellationToken)) + public override async Task UnsubscribeAsync (CancellationToken cancellationToken = default) { - if ((Engine.Capabilities & ImapCapabilities.Status) == 0) - throw new NotSupportedException ("The IMAP server does not support the STATUS command."); + var ic = QueueUnsubscribeCommand (cancellationToken); + + await Engine.RunAsync (ic).ConfigureAwait (false); + + ProcessUnsubscribeResponse (ic); + } + bool TryQueueGetSubfoldersCommand (StatusItems items, bool subscribedOnly, CancellationToken cancellationToken, [NotNullWhen (true)] out ImapCommand? ic, [NotNullWhen (true)] out List? list, out bool status) + { CheckState (false, false); - if (items == StatusItems.None) - return; + // Any folder with a nil directory separator cannot have children. + if (DirectorySeparator == '\0') { + status = false; + list = null; + ic = null; + return false; + } - var command = string.Format ("STATUS %F ({0})\r\n", Engine.GetStatusQuery (items)); - var ic = Engine.QueueCommand (cancellationToken, null, command, this); + // Note: folder names can contain wildcards (including '*' and '%'), so replace '*' with '%' + // in order to reduce the list of folders returned by our LIST command. + var pattern = new StringBuilder (EncodedName.Length + 2); + pattern.Append (EncodedName); + for (int i = 0; i < pattern.Length; i++) { + if (pattern[i] == '*') + pattern[i] = '%'; + } + if (pattern.Length > 0) + pattern.Append (DirectorySeparator); + pattern.Append ('%'); - Engine.Wait (ic); + status = items != StatusItems.None; + list = new List (); - ProcessResponseCodes (ic, this); + var command = new StringBuilder (); + var returnsSubscribed = false; + var lsub = subscribedOnly; + + if (subscribedOnly) { + if ((Engine.Capabilities & ImapCapabilities.ListExtended) != 0) { + command.Append ("LIST (SUBSCRIBED)"); + returnsSubscribed = true; + lsub = false; + } else { + command.Append ("LSUB"); + } + } else { + command.Append ("LIST"); + } + + command.Append (" \"\" %S"); + + if (!lsub) { + if (items != StatusItems.None && (Engine.Capabilities & ImapCapabilities.ListStatus) != 0) { + command.Append (" RETURN ("); + + if ((Engine.Capabilities & ImapCapabilities.ListExtended) != 0) { + if (!subscribedOnly) { + command.Append ("SUBSCRIBED "); + returnsSubscribed = true; + } + command.Append ("CHILDREN "); + } + + command.Append ("STATUS ("); + command.Append (Engine.GetStatusQuery (items)); + command.Append ("))"); + status = false; + } else if ((Engine.Capabilities & ImapCapabilities.ListExtended) != 0) { + command.Append (" RETURN ("); + if (!subscribedOnly) { + command.Append ("SUBSCRIBED "); + returnsSubscribed = true; + } + command.Append ("CHILDREN"); + command.Append (')'); + } + } + + command.Append ("\r\n"); + + ic = new ImapCommand (Engine, cancellationToken, null, command.ToString (), pattern.ToString ()); + ic.RegisterUntaggedHandler (lsub ? "LSUB" : "LIST", ImapUtils.UntaggedListHandler); + ic.ListReturnsSubscribed = returnsSubscribed; + ic.UserData = list; + ic.Lsub = lsub; + + Engine.QueueCommand (ic); - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("STATUS", ic); + return true; } - static void UntaggedAcl (ImapEngine engine, ImapCommand ic, int index) + IList ProcessGetSubfoldersResponse (ImapCommand ic, List list, out bool unparented) { - string format = string.Format (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ACL", "{0}"); - var acl = (AccessControlList) ic.UserData; - string name, rights; - ImapToken token; + // Note: Due to the fact that folders can contain wildcards in them, we'll need to + // filter out any folders that are not children of this folder. + var prefix = FullName.Length > 0 ? FullName + DirectorySeparator : string.Empty; + prefix = ImapUtils.CanonicalizeMailboxName (prefix, DirectorySeparator); + var children = new List (); + unparented = false; - // read the mailbox name - ReadStringToken (engine, format, ic.CancellationToken); + foreach (var folder in list) { + var canonicalFullName = ImapUtils.CanonicalizeMailboxName (folder.FullName, folder.DirectorySeparator); + var canonicalName = ImapUtils.IsInbox (folder.FullName) ? "INBOX" : folder.Name; - do { - name = ReadStringToken (engine, format, ic.CancellationToken); - rights = ReadStringToken (engine, format, ic.CancellationToken); + if (!canonicalFullName.StartsWith (prefix, StringComparison.Ordinal)) { + unparented |= folder.ParentFolder == null; + continue; + } - acl.Add (new AccessControl (name, rights)); + if (string.Compare (canonicalFullName, prefix.Length, canonicalName, 0, canonicalName.Length, StringComparison.Ordinal) != 0) { + unparented |= folder.ParentFolder == null; + continue; + } - token = engine.PeekToken (ic.CancellationToken); - } while (token.Type != ImapTokenType.Eoln); + folder.ParentFolder = this; + children.Add (folder); + } + + ProcessResponseCodes (ic, null); + + ic.ThrowIfNotOk (ic.Lsub ? "LSUB" : "LIST"); + + return children; } /// - /// Get the complete access control list for the folder. + /// Get the subfolders. /// /// - /// Gets the complete access control list for the folder. + /// Gets the subfolders. /// - /// The access control list. + /// The subfolders. + /// The status items to pre-populate. + /// If set to , only subscribed folders will be listed. /// The cancellation token. /// /// The has been disposed. @@ -1322,9 +1753,6 @@ static void UntaggedAcl (ImapEngine engine, ImapCommand ic, int index) /// /// The is not authenticated. /// - /// - /// The IMAP server does not support the ACL extension. - /// /// /// The operation was canceled via the cancellation token. /// @@ -1335,63 +1763,42 @@ static void UntaggedAcl (ImapEngine engine, ImapCommand ic, int index) /// The server's response contained unexpected tokens. /// /// - /// The command failed. + /// The server replied with a NO or BAD response. /// - public override AccessControlList GetAccessControlList (CancellationToken cancellationToken = default (CancellationToken)) - { - if ((Engine.Capabilities & ImapCapabilities.Acl) == 0) - throw new NotSupportedException ("The IMAP server does not support the ACL extension."); - - CheckState (false, false); - - var ic = new ImapCommand (Engine, cancellationToken, null, "GETACL %F\r\n", this); - ic.RegisterUntaggedHandler ("ACL", UntaggedAcl); - ic.UserData = new AccessControlList (); - - Engine.QueueCommand (ic); - Engine.Wait (ic); - - ProcessResponseCodes (ic, null); - - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("GETACL", ic); - - return (AccessControlList) ic.UserData; - } - - static void UntaggedListRights (ImapEngine engine, ImapCommand ic, int index) + public override IList GetSubfolders (StatusItems items, bool subscribedOnly = false, CancellationToken cancellationToken = default) { - string format = string.Format (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "LISTRIGHTS", "{0}"); - var access = (AccessRights) ic.UserData; - ImapToken token; + if (!TryQueueGetSubfoldersCommand (items, subscribedOnly, cancellationToken, out var ic, out var list, out var status)) + return Array.Empty (); - // read the mailbox name - ReadStringToken (engine, format, ic.CancellationToken); + Engine.Run (ic); - // read the identity name - ReadStringToken (engine, format, ic.CancellationToken); + var children = ProcessGetSubfoldersResponse (ic, list, out var unparented); - do { - var rights = ReadStringToken (engine, format, ic.CancellationToken); + // Note: if any folders returned in the LIST command are unparented, have the ImapEngine look up their + // parent folders now so that they are not left in an inconsistent state. + if (unparented) + Engine.LookupParentFolders (list, cancellationToken); - access.AddRange (rights); + if (status) { + for (int i = 0; i < children.Count; i++) { + if (children[i].Exists) + ((ImapFolder) children[i]).Status (items, false, cancellationToken); + } + } - token = engine.PeekToken (ic.CancellationToken); - } while (token.Type != ImapTokenType.Eoln); + return children; } /// - /// Get the access rights for a particular identifier. + /// Asynchronously get the subfolders. /// /// - /// Gets the access rights for a particular identifier. + /// Gets the subfolders. /// - /// The access rights. - /// The identifier name. + /// The subfolders. + /// The status items to pre-populate. + /// If set to , only subscribed folders will be listed. /// The cancellation token. - /// - /// is null. - /// /// /// The has been disposed. /// @@ -1401,9 +1808,6 @@ static void UntaggedListRights (ImapEngine engine, ImapCommand ic, int index) /// /// The is not authenticated. /// - /// - /// The IMAP server does not support the ACL extension. - /// /// /// The operation was canceled via the cancellation token. /// @@ -1414,53 +1818,103 @@ static void UntaggedListRights (ImapEngine engine, ImapCommand ic, int index) /// The server's response contained unexpected tokens. /// /// - /// The command failed. + /// The server replied with a NO or BAD response. /// - public override AccessRights GetAccessRights (string name, CancellationToken cancellationToken = default (CancellationToken)) + public override async Task> GetSubfoldersAsync (StatusItems items, bool subscribedOnly = false, CancellationToken cancellationToken = default) + { + if (!TryQueueGetSubfoldersCommand (items, subscribedOnly, cancellationToken, out var ic, out var list, out var status)) + return Array.Empty (); + + await Engine.RunAsync (ic).ConfigureAwait (false); + + var children = ProcessGetSubfoldersResponse (ic, list, out var unparented); + + // Note: if any folders returned in the LIST command are unparented, have the ImapEngine look up their + // parent folders now so that they are not left in an inconsistent state. + if (unparented) + await Engine.LookupParentFoldersAsync (list, cancellationToken).ConfigureAwait (false); + + if (status) { + for (int i = 0; i < children.Count; i++) { + if (children[i].Exists) + await ((ImapFolder) children[i]).StatusAsync (items, false, cancellationToken).ConfigureAwait (false); + } + } + + return children; + } + + bool TryQueueGetSubfolderCommand (string name, CancellationToken cancellationToken, [NotNullWhen (true)] out ImapCommand? ic, [NotNullWhen (true)] out List? list, [NotNullWhen (true)] out string? fullName, [NotNullWhen (true)] out string? encodedName, out ImapFolder? folder) { if (name == null) throw new ArgumentNullException (nameof (name)); - if ((Engine.Capabilities & ImapCapabilities.Acl) == 0) - throw new NotSupportedException ("The IMAP server does not support the ACL extension."); + if (!ImapEngine.IsValidMailboxName (name, DirectorySeparator)) + throw new ArgumentException ("The name of the subfolder is invalid.", nameof (name)); CheckState (false, false); - var ic = new ImapCommand (Engine, cancellationToken, null, "LISTRIGHTS %F %S\r\n", this, name); - ic.RegisterUntaggedHandler ("LISTRIGHTS", UntaggedListRights); - ic.UserData = new AccessRights (); + // Any folder with a nil directory separator cannot have children. + if (DirectorySeparator == '\0') { + encodedName = null; + fullName = null; + folder = null; + list = null; + ic = null; + return false; + } - Engine.QueueCommand (ic); - Engine.Wait (ic); + fullName = FullName.Length > 0 ? FullName + DirectorySeparator + name : name; + encodedName = Engine.EncodeMailboxName (fullName); - ProcessResponseCodes (ic, null); + if (Engine.TryGetCachedFolder (encodedName, out folder)) { + list = null; + ic = null; + return false; + } - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("LISTRIGHTS", ic); + // Note: folder names can contain wildcards (including '*' and '%'), so replace '*' with '%' + // in order to reduce the list of folders returned by our LIST command. + var pattern = encodedName.Replace ('*', '%'); - return (AccessRights) ic.UserData; + ic = new ImapCommand (Engine, cancellationToken, null, "LIST \"\" %S\r\n", pattern); + ic.RegisterUntaggedHandler ("LIST", ImapUtils.UntaggedListHandler); + ic.UserData = list = new List (); + + Engine.QueueCommand (ic); + + return true; } - static void UntaggedMyRights (ImapEngine engine, ImapCommand ic, int index) + ImapFolder? ProcessGetSubfolderResponse (ImapCommand ic, List list, string encodedName) { - string format = string.Format (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "MYRIGHTS", "{0}"); - var access = (AccessRights) ic.UserData; + ImapFolder? folder; - // read the mailbox name - ReadStringToken (engine, format, ic.CancellationToken); + ProcessResponseCodes (ic, null); - // read the access rights - access.AddRange (ReadStringToken (engine, format, ic.CancellationToken)); + ic.ThrowIfNotOk ("LIST"); + + if ((folder = ImapEngine.GetFolder (list, encodedName)) != null) + folder.ParentFolder = this; + + return folder; } /// - /// Get the access rights for the current authenticated user. + /// Get the specified subfolder. /// /// - /// Gets the access rights for the current authenticated user. + /// Gets the specified subfolder. /// - /// The access rights. + /// The subfolder. + /// The name of the subfolder. /// The cancellation token. + /// + /// is . + /// + /// + /// is either an empty string or contains the . + /// /// /// The has been disposed. /// @@ -1470,4224 +1924,56 @@ static void UntaggedMyRights (ImapEngine engine, ImapCommand ic, int index) /// /// The is not authenticated. /// - /// - /// The IMAP server does not support the ACL extension. - /// /// /// The operation was canceled via the cancellation token. /// /// /// An I/O error occurred. /// + /// + /// The requested folder could not be found. + /// /// /// The server's response contained unexpected tokens. /// /// - /// The command failed. + /// The server replied with a NO or BAD response. /// - public override AccessRights GetMyAccessRights (CancellationToken cancellationToken = default (CancellationToken)) - { - if ((Engine.Capabilities & ImapCapabilities.Acl) == 0) - throw new NotSupportedException ("The IMAP server does not support the ACL extension."); - - CheckState (false, false); - - var ic = new ImapCommand (Engine, cancellationToken, null, "MYRIGHTS %F\r\n", this); - ic.RegisterUntaggedHandler ("MYRIGHTS", UntaggedMyRights); - ic.UserData = new AccessRights (); - - Engine.QueueCommand (ic); - Engine.Wait (ic); - - ProcessResponseCodes (ic, null); - - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("MYRIGHTS", ic); - - return (AccessRights) ic.UserData; - } - - void ModifyAccessRights (string name, AccessRights rights, string action, CancellationToken cancellationToken) + public override IMailFolder GetSubfolder (string name, CancellationToken cancellationToken = default) { - if ((Engine.Capabilities & ImapCapabilities.Acl) == 0) - throw new NotSupportedException ("The IMAP server does not support the ACL extension."); + if (!TryQueueGetSubfolderCommand (name, cancellationToken, out var ic, out var list, out var fullName, out var encodedName, out var folder)) + return folder ?? throw new FolderNotFoundException (name); - CheckState (false, false); + Engine.Run (ic); - var ic = Engine.QueueCommand (cancellationToken, null, "SETACL %F %S %S\r\n", this, name, action + rights); + folder = ProcessGetSubfolderResponse (ic, list, encodedName); - Engine.Wait (ic); + if (list.Count > 1 || folder == null) { + // Note: if any folders returned in the LIST command are unparented, have the ImapEngine look up their + // parent folders now so that they are not left in an inconsistent state. + Engine.LookupParentFolders (list, cancellationToken); + } - ProcessResponseCodes (ic, null); + if (folder == null) + throw new FolderNotFoundException (fullName); - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("SETACL", ic); + return folder; } /// - /// Add access rights for the specified identity. + /// Asynchronously get the specified subfolder. /// /// - /// Adds the given access rights for the specified identity. - /// - /// The identity name. - /// The access rights. - /// The cancellation token. - /// - /// is null. - /// -or- - /// is null. - /// - /// - /// No rights were specified. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The IMAP server does not support the ACL extension. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public override void AddAccessRights (string name, AccessRights rights, CancellationToken cancellationToken = default (CancellationToken)) - { - if (name == null) - throw new ArgumentNullException (nameof (name)); - - if (rights == null) - throw new ArgumentNullException (nameof (rights)); - - if (rights.Count == 0) - throw new ArgumentException ("No rights were specified.", nameof (rights)); - - ModifyAccessRights (name, rights, "+", cancellationToken); - } - - /// - /// Remove access rights for the specified identity. - /// - /// - /// Removes the given access rights for the specified identity. - /// - /// The identity name. - /// The access rights. - /// The cancellation token. - /// - /// is null. - /// -or- - /// is null. - /// - /// - /// No rights were specified. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The IMAP server does not support the ACL extension. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public override void RemoveAccessRights (string name, AccessRights rights, CancellationToken cancellationToken = default (CancellationToken)) - { - if (name == null) - throw new ArgumentNullException (nameof (name)); - - if (rights == null) - throw new ArgumentNullException (nameof (rights)); - - if (rights.Count == 0) - throw new ArgumentException ("No rights were specified.", nameof (rights)); - - ModifyAccessRights (name, rights, "-", cancellationToken); - } - - /// - /// Set the access rights for the specified identity. - /// - /// - /// Sets the access rights for the specified identity. - /// - /// The identity name. - /// The access rights. - /// The cancellation token. - /// - /// is null. - /// -or- - /// is null. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The IMAP server does not support the ACL extension. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public override void SetAccessRights (string name, AccessRights rights, CancellationToken cancellationToken = default (CancellationToken)) - { - if (name == null) - throw new ArgumentNullException (nameof (name)); - - if (rights == null) - throw new ArgumentNullException (nameof (rights)); - - ModifyAccessRights (name, rights, string.Empty, cancellationToken); - } - - /// - /// Remove all access rights for the given identity. - /// - /// - /// Removes all access rights for the given identity. - /// - /// The identity name. - /// The cancellation token. - /// - /// is null. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The IMAP server does not support the ACL extension. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The command failed. - /// - public override void RemoveAccess (string name, CancellationToken cancellationToken = default (CancellationToken)) - { - if (name == null) - throw new ArgumentNullException (nameof (name)); - - if ((Engine.Capabilities & ImapCapabilities.Acl) == 0) - throw new NotSupportedException ("The IMAP server does not support the ACL extension."); - - CheckState (false, false); - - var ic = Engine.QueueCommand (cancellationToken, null, "DELETEACL %F %S\r\n", this, name); - - Engine.Wait (ic); - - ProcessResponseCodes (ic, null); - - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("DELETEACL", ic); - } - - static string ReadStringToken (ImapEngine engine, string format, CancellationToken cancellationToken) - { - var token = engine.ReadToken (cancellationToken); - - switch (token.Type) { - case ImapTokenType.Literal: return engine.ReadLiteral (cancellationToken); - case ImapTokenType.QString: return (string) token.Value; - case ImapTokenType.Atom: return (string) token.Value; - default: - throw ImapEngine.UnexpectedToken (format, token); - } - } - - class Quota - { - public uint? MessageLimit; - public uint? StorageLimit; - public uint? CurrentMessageCount; - public uint? CurrentStorageSize; - } - - class QuotaContext - { - public QuotaContext () - { - Quotas = new Dictionary (); - QuotaRoots = new List (); - } - - public IList QuotaRoots { - get; private set; - } - - public IDictionary Quotas { - get; private set; - } - } - - static void UntaggedQuotaRoot (ImapEngine engine, ImapCommand ic, int index) - { - var format = string.Format (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "QUOTAROOT", "{0}"); - var ctx = (QuotaContext) ic.UserData; - - // The first token should be the mailbox name - ReadStringToken (engine, format, ic.CancellationToken); - - // ...followed by 0 or more quota roots - var token = engine.PeekToken (ic.CancellationToken); - - while (token.Type != ImapTokenType.Eoln) { - var root = ReadStringToken (engine, format, ic.CancellationToken); - ctx.QuotaRoots.Add (root); - - token = engine.PeekToken (ic.CancellationToken); - } - } - - static void UntaggedQuota (ImapEngine engine, ImapCommand ic, int index) - { - var format = string.Format (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "QUOTA", "{0}"); - var quotaRoot = ReadStringToken (engine, format, ic.CancellationToken); - var ctx = (QuotaContext) ic.UserData; - var quota = new Quota (); - - var token = engine.ReadToken (ic.CancellationToken); - - if (token.Type != ImapTokenType.OpenParen) - throw ImapEngine.UnexpectedToken (format, token); - - while (token.Type != ImapTokenType.CloseParen) { - uint used, limit; - string resource; - - token = engine.ReadToken (ic.CancellationToken); - - if (token.Type != ImapTokenType.Atom) - throw ImapEngine.UnexpectedToken (format, token); - - resource = (string) token.Value; - - token = engine.ReadToken (ic.CancellationToken); - - if (token.Type != ImapTokenType.Atom || !uint.TryParse ((string) token.Value, out used)) - throw ImapEngine.UnexpectedToken (format, token); - - token = engine.ReadToken (ic.CancellationToken); - - if (token.Type != ImapTokenType.Atom || !uint.TryParse ((string) token.Value, out limit)) - throw ImapEngine.UnexpectedToken (format, token); - - switch (resource.ToUpperInvariant ()) { - case "MESSAGE": - quota.CurrentMessageCount = used; - quota.MessageLimit = limit; - break; - case "STORAGE": - quota.CurrentStorageSize = used; - quota.StorageLimit = limit; - break; - } - - token = engine.PeekToken (ic.CancellationToken); - } - - // read the closing paren - engine.ReadToken (ic.CancellationToken); - - ctx.Quotas[quotaRoot] = quota; - } - - /// - /// Get the quota information for the folder. - /// - /// - /// Gets the quota information for the folder. - /// To determine if a quotas are supported, check the - /// property. - /// - /// The folder quota. - /// The cancellation token. - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The IMAP server does not support the QUOTA extension. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The server replied with a NO or BAD response. - /// - public override FolderQuota GetQuota (CancellationToken cancellationToken = default (CancellationToken)) - { - CheckState (false, false); - - if ((Engine.Capabilities & ImapCapabilities.Quota) == 0) - throw new NotSupportedException ("The IMAP server does not support the QUOTA extension."); - - var ic = new ImapCommand (Engine, cancellationToken, null, "GETQUOTAROOT %F\r\n", this); - var ctx = new QuotaContext (); - - ic.RegisterUntaggedHandler ("QUOTAROOT", UntaggedQuotaRoot); - ic.RegisterUntaggedHandler ("QUOTA", UntaggedQuota); - ic.UserData = ctx; - - Engine.QueueCommand (ic); - Engine.Wait (ic); - - ProcessResponseCodes (ic, null); - - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("GETQUOTAROOT", ic); - - for (int i = 0; i < ctx.QuotaRoots.Count; i++) { - var encodedName = ctx.QuotaRoots[i]; - ImapFolder quotaRoot; - Quota quota; - - if (!ctx.Quotas.TryGetValue (encodedName, out quota)) - continue; - - quotaRoot = Engine.GetQuotaRootFolder (encodedName, cancellationToken); - - return new FolderQuota (quotaRoot) { - CurrentMessageCount = quota.CurrentMessageCount, - CurrentStorageSize = quota.CurrentStorageSize, - MessageLimit = quota.MessageLimit, - StorageLimit = quota.StorageLimit - }; - } - - return new FolderQuota (null); - } - - /// - /// Set the quota limits for the folder. - /// - /// - /// Sets the quota limits for the folder. - /// To determine if a quotas are supported, check the - /// property. - /// - /// The folder quota. - /// If not null, sets the maximum number of messages to allow. - /// If not null, sets the maximum storage size (in kilobytes). - /// The cancellation token. - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The IMAP server does not support the QUOTA extension. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The server replied with a NO or BAD response. - /// - public override FolderQuota SetQuota (uint? messageLimit, uint? storageLimit, CancellationToken cancellationToken = default (CancellationToken)) - { - CheckState (false, false); - - if ((Engine.Capabilities & ImapCapabilities.Quota) == 0) - throw new NotSupportedException ("The IMAP server does not support the QUOTA extension."); - - var command = new StringBuilder ("SETQUOTA %F ("); - if (messageLimit.HasValue) - command.AppendFormat ("MESSAGE {0} ", messageLimit.Value); - if (storageLimit.HasValue) - command.AppendFormat ("STORAGE {0} ", storageLimit.Value); - command[command.Length - 1] = ')'; - command.Append ("\r\n"); - - var ic = new ImapCommand (Engine, cancellationToken, null, command.ToString (), this); - var ctx = new QuotaContext (); - Quota quota; - - ic.RegisterUntaggedHandler ("QUOTA", UntaggedQuota); - ic.UserData = ctx; - - Engine.QueueCommand (ic); - Engine.Wait (ic); - - ProcessResponseCodes (ic, null); - - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("SETQUOTA", ic); - - if (ctx.Quotas.TryGetValue (EncodedName, out quota)) { - return new FolderQuota (this) { - CurrentMessageCount = quota.CurrentMessageCount, - CurrentStorageSize = quota.CurrentStorageSize, - MessageLimit = quota.MessageLimit, - StorageLimit = quota.StorageLimit - }; - } - - return new FolderQuota (null); - } - - /// - /// Gets the specified metadata. - /// - /// - /// Gets the specified metadata. - /// - /// The requested metadata value. - /// The metadata tag. - /// The cancellation token. - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The IMAP server does not support the METADATA extension. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The server replied with a NO or BAD response. - /// - public override string GetMetadata (MetadataTag tag, CancellationToken cancellationToken = default (CancellationToken)) - { - CheckState (false, false); - - if ((Engine.Capabilities & ImapCapabilities.Metadata) == 0) - throw new NotSupportedException ("The IMAP server does not support the METADATA extension."); - - var ic = new ImapCommand (Engine, cancellationToken, null, "GETMETADATA %F %S\r\n", this, tag.Id); - ic.RegisterUntaggedHandler ("METADATA", ImapUtils.ParseMetadata); - var metadata = new MetadataCollection (); - ic.UserData = metadata; - - Engine.QueueCommand (ic); - Engine.Wait (ic); - - ProcessResponseCodes (ic, null); - - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("GETMETADATA", ic); - - for (int i = 0; i < metadata.Count; i++) { - if (metadata[i].Tag.Id == tag.Id) - return metadata[i].Value; - } - - return null; - } - - /// - /// Gets the specified metadata. - /// - /// - /// Gets the specified metadata. - /// - /// The requested metadata. - /// The metadata options. - /// The metadata tags. - /// The cancellation token. - /// - /// is null. - /// -or- - /// is null. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The IMAP server does not support the METADATA extension. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The server replied with a NO or BAD response. - /// - public override MetadataCollection GetMetadata (MetadataOptions options, IEnumerable tags, CancellationToken cancellationToken = default (CancellationToken)) - { - if (options == null) - throw new ArgumentNullException (nameof (options)); - - if (tags == null) - throw new ArgumentNullException (nameof (tags)); - - CheckState (false, false); - - if ((Engine.Capabilities & ImapCapabilities.Metadata) == 0) - throw new NotSupportedException ("The IMAP server does not support the METADATA extension."); - - var command = new StringBuilder ("GETMETADATA %F"); - var args = new List (); - bool hasOptions = false; - - if (options.MaxSize.HasValue || options.Depth != 0) { - command.Append (" ("); - if (options.MaxSize.HasValue) - command.AppendFormat ("MAXSIZE {0} ", options.MaxSize.Value); - if (options.Depth > 0) - command.AppendFormat ("DEPTH {0} ", options.Depth == int.MaxValue ? "infinity" : "1"); - command[command.Length - 1] = ')'; - command.Append (' '); - hasOptions = true; - } - - args.Add (this); - - int startIndex = command.Length; - foreach (var tag in tags) { - command.Append (" %S"); - args.Add (tag.Id); - } - - if (hasOptions) { - command[startIndex] = '('; - command.Append (')'); - } - - command.Append ("\r\n"); - - if (args.Count == 1) - return new MetadataCollection (); - - var ic = new ImapCommand (Engine, cancellationToken, null, command.ToString (), args.ToArray ()); - ic.RegisterUntaggedHandler ("METADATA", ImapUtils.ParseMetadata); - ic.UserData = new MetadataCollection (); - options.LongEntries = 0; - - Engine.QueueCommand (ic); - Engine.Wait (ic); - - ProcessResponseCodes (ic, null); - - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("GETMETADATA", ic); - - if (ic.RespCodes.Count > 0 && ic.RespCodes[ic.RespCodes.Count - 1].Type == ImapResponseCodeType.Metadata) { - var metadata = (MetadataResponseCode) ic.RespCodes[ic.RespCodes.Count - 1]; - - if (metadata.SubType == MetadataResponseCodeSubType.LongEntries) - options.LongEntries = metadata.Value; - } - - return (MetadataCollection) ic.UserData; - } - - /// - /// Sets the specified metadata. - /// - /// - /// Sets the specified metadata. - /// - /// The metadata. - /// The metadata. - /// The cancellation token. - /// - /// is null. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The IMAP server does not support the METADATA extension. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The server replied with a NO or BAD response. - /// - public override void SetMetadata (MetadataCollection metadata, CancellationToken cancellationToken = default (CancellationToken)) - { - if (metadata == null) - throw new ArgumentNullException (nameof (metadata)); - - CheckState (false, false); - - if ((Engine.Capabilities & ImapCapabilities.Metadata) == 0) - throw new NotSupportedException ("The IMAP server does not support the METADATA extension."); - - if (metadata.Count == 0) - return; - - var command = new StringBuilder ("SETMETADATA %F ("); - var args = new List (); - - args.Add (this); - - for (int i = 0; i < metadata.Count; i++) { - if (i > 0) - command.Append (' '); - - if (metadata[i].Value != null) { - command.Append ("%S %S"); - args.Add (metadata[i].Tag.Id); - args.Add (metadata[i].Value); - } else { - command.Append ("%S NIL"); - args.Add (metadata[i].Tag.Id); - } - } - command.Append (")\r\n"); - - var ic = new ImapCommand (Engine, cancellationToken, null, command.ToString (), args.ToArray ()); - - Engine.QueueCommand (ic); - Engine.Wait (ic); - - ProcessResponseCodes (ic, null); - - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("SETMETADATA", ic); - } - - /// - /// Expunges the folder, permanently removing all messages marked for deletion. - /// - /// - /// The EXPUNGE command permanently removes all messages in the folder - /// that have the flag set. - /// For more information about the EXPUNGE command, see - /// rfc3501. - /// Normally, a event will be emitted - /// for each message that is expunged. However, if the IMAP server supports the QRESYNC extension - /// and it has been enabled via the - /// method, then the event will be emitted rather than - /// the event. - /// - /// The cancellation token. - /// - /// The has been disposed. - /// - /// - /// The is not currently open in read-write mode. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The server replied with a NO or BAD response. - /// - public override void Expunge (CancellationToken cancellationToken = default (CancellationToken)) - { - CheckState (true, true); - - var ic = Engine.QueueCommand (cancellationToken, this, "EXPUNGE\r\n"); - - Engine.Wait (ic); - - ProcessResponseCodes (ic, null); - - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("EXPUNGE", ic); - } - - /// - /// Expunge the specified uids, permanently removing them from the folder. - /// - /// - /// Expunges the specified uids, permanently removing them from the folder. - /// If the IMAP server supports the UIDPLUS extension (check the - /// for the - /// flag), then this operation is atomic. Otherwise, MailKit implements this operation - /// by first searching for the full list of message uids in the folder that are marked for - /// deletion, unmarking the set of message uids that are not within the specified list of - /// uids to be be expunged, expunging the folder (thus expunging the requested uids), and - /// finally restoring the deleted flag on the collection of message uids that were originally - /// marked for deletion that were not included in the list of uids provided. For this reason, - /// it is advisable for clients that wish to maintain state to implement this themselves when - /// the IMAP server does not support the UIDPLUS extension. - /// For more information about the UID EXPUNGE command, see - /// rfc4315. - /// Normally, a event will be emitted - /// for each message that is expunged. However, if the IMAP server supports the QRESYNC extension - /// and it has been enabled via the - /// method, then the event will be emitted rather than - /// the event. - /// - /// The message uids. - /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the is invalid. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The is not currently open in read-write mode. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The server replied with a NO or BAD response. - /// - public override void Expunge (IList uids, CancellationToken cancellationToken = default (CancellationToken)) - { - if (uids == null) - throw new ArgumentNullException (nameof (uids)); - - CheckState (true, true); - - if (uids.Count == 0) - return; - - if ((Engine.Capabilities & ImapCapabilities.UidPlus) == 0) { - // get the list of messages marked for deletion that should not be expunged - var unmark = Search (SearchQuery.Deleted.And (SearchQuery.Not (SearchQuery.Uids (uids))), cancellationToken); - - if (unmark.Count > 0) { - // clear the \Deleted flag on all messages except the ones that are to be expunged - RemoveFlags (unmark, MessageFlags.Deleted, true, cancellationToken); - } - - // expunge the folder - Expunge (cancellationToken); - - if (unmark.Count > 0) { - // restore the \Deleted flags - AddFlags (unmark, MessageFlags.Deleted, true, cancellationToken); - } - - return; - } - - var set = ImapUtils.FormatUidSet (uids); - var command = string.Format ("UID EXPUNGE {0}\r\n", set); - var ic = Engine.QueueCommand (cancellationToken, this, command); - - Engine.Wait (ic); - - ProcessResponseCodes (ic, null); - - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("EXPUNGE", ic); - } - - ImapCommand QueueAppend (FormatOptions options, MimeMessage message, MessageFlags flags, DateTimeOffset? date, CancellationToken cancellationToken, ITransferProgress progress) - { - string format = "APPEND %F"; - - if ((flags & SettableFlags) != 0) - format += " " + ImapUtils.FormatFlagsList (flags, 0); - - if (date.HasValue) - format += " \"" + ImapUtils.FormatInternalDate (date.Value) + "\""; - - format += " %L\r\n"; - - var ic = new ImapCommand (Engine, cancellationToken, null, options, format, this, message); - ic.Progress = progress; - - Engine.QueueCommand (ic); - - return ic; - } - - /// - /// Appends the specified message to the folder. - /// - /// - /// Appends the specified message to the folder and returns the UniqueId assigned to the message. - /// - /// The UID of the appended message, if available; otherwise, null. - /// The formatting options. - /// The message. - /// The message flags. - /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is null. - /// -or- - /// is null. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// Internationalized formatting was requested but has not been enabled. - /// - /// - /// The does not exist. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// Internationalized formatting was requested but is not supported by the server. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The server replied with a NO or BAD response. - /// - public override UniqueId? Append (FormatOptions options, MimeMessage message, MessageFlags flags = MessageFlags.None, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) - { - if (options == null) - throw new ArgumentNullException (nameof (options)); - - if (message == null) - throw new ArgumentNullException (nameof (message)); - - CheckState (false, false); - - if (options.International && (Engine.Capabilities & ImapCapabilities.UTF8Accept) == 0) - throw new NotSupportedException ("The IMAP server does not support the UTF8 extension."); - - var format = options.Clone (); - format.NewLineFormat = NewLineFormat.Dos; - - if ((Engine.Capabilities & ImapCapabilities.UTF8Only) == ImapCapabilities.UTF8Only) - format.International = true; - - if (format.International && !Engine.UTF8Enabled) - throw new InvalidOperationException ("The UTF8 extension has not been enabled."); - - var ic = QueueAppend (format, message, flags, null, cancellationToken, progress); - - Engine.Wait (ic); - - ProcessResponseCodes (ic, this); - - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("APPEND", ic); - - var append = ic.RespCodes.OfType ().FirstOrDefault (); - - if (append != null) - return append.UidSet[0]; - - return null; - } - - /// - /// Appends the specified message to the folder. - /// - /// - /// Appends the specified message to the folder and returns the UniqueId assigned to the message. - /// - /// The UID of the appended message, if available; otherwise, null. - /// The formatting options. - /// The message. - /// The message flags. - /// The received date of the message. - /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is null. - /// -or- - /// is null. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// Internationalized formatting was requested but has not been enabled. - /// - /// - /// The does not exist. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// Internationalized formatting was requested but is not supported by the server. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The server replied with a NO or BAD response. - /// - public override UniqueId? Append (FormatOptions options, MimeMessage message, MessageFlags flags, DateTimeOffset date, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) - { - if (options == null) - throw new ArgumentNullException (nameof (options)); - - if (message == null) - throw new ArgumentNullException (nameof (message)); - - CheckState (false, false); - - if (options.International && (Engine.Capabilities & ImapCapabilities.UTF8Accept) == 0) - throw new NotSupportedException ("The IMAP server does not support the UTF8 extension."); - - var format = options.Clone (); - format.NewLineFormat = NewLineFormat.Dos; - - if ((Engine.Capabilities & ImapCapabilities.UTF8Only) == ImapCapabilities.UTF8Only) - format.International = true; - - if (format.International && !Engine.UTF8Enabled) - throw new InvalidOperationException ("The UTF8 extension has not been enabled."); - - var ic = QueueAppend (format, message, flags, date, cancellationToken, progress); - - Engine.Wait (ic); - - ProcessResponseCodes (ic, this); - - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("APPEND", ic); - - var append = ic.RespCodes.OfType ().FirstOrDefault (); - - if (append != null) - return append.UidSet[0]; - - return null; - } - - ImapCommand QueueMultiAppend (FormatOptions options, IList messages, IList flags, IList dates, CancellationToken cancellationToken, ITransferProgress progress) - { - var args = new List (); - string format = "APPEND %F"; - - args.Add (this); - - for (int i = 0; i < messages.Count; i++) { - if ((flags[i] & SettableFlags) != 0) - format += " " + ImapUtils.FormatFlagsList (flags[i], 0); - - if (dates != null) - format += " \"" + ImapUtils.FormatInternalDate (dates[i]) + "\""; - - format += " %L"; - - args.Add (messages[i]); - } - - format += "\r\n"; - - var ic = new ImapCommand (Engine, cancellationToken, null, options, format, args.ToArray ()); - ic.Progress = progress; - - Engine.QueueCommand (ic); - - return ic; - } - - /// - /// Appends the specified messages to the folder. - /// - /// - /// Appends the specified messages to the folder and returns the UniqueIds assigned to the messages. - /// - /// The UIDs of the appended messages, if available; otherwise an empty array. - /// The formatting options. - /// The list of messages to append to the folder. - /// The message flags to use for each message. - /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is null. - /// -or- - /// is null. - /// -or- - /// is null. - /// - /// - /// One or more of the is null. - /// -or- - /// The number of messages does not match the number of flags. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// Internationalized formatting was requested but has not been enabled. - /// - /// - /// The does not exist. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// Internationalized formatting was requested but is not supported by the server. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The server replied with a NO or BAD response. - /// - public override IList Append (FormatOptions options, IList messages, IList flags, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) - { - if (options == null) - throw new ArgumentNullException (nameof (options)); - - if (messages == null) - throw new ArgumentNullException (nameof (messages)); - - for (int i = 0; i < messages.Count; i++) { - if (messages[i] == null) - throw new ArgumentException ("One or more of the messages is null."); - } - - if (flags == null) - throw new ArgumentNullException (nameof (flags)); - - if (messages.Count != flags.Count) - throw new ArgumentException ("The number of messages and the number of flags must be equal."); - - CheckState (false, false); - - if (options.International && (Engine.Capabilities & ImapCapabilities.UTF8Accept) == 0) - throw new NotSupportedException ("The IMAP server does not support the UTF8 extension."); - - var format = options.Clone (); - format.NewLineFormat = NewLineFormat.Dos; - - if ((Engine.Capabilities & ImapCapabilities.UTF8Only) == ImapCapabilities.UTF8Only) - format.International = true; - - if (format.International && !Engine.UTF8Enabled) - throw new InvalidOperationException ("The UTF8 extension has not been enabled."); - - if (messages.Count == 0) - return new UniqueId[0]; - - if ((Engine.Capabilities & ImapCapabilities.MultiAppend) != 0) { - var ic = QueueMultiAppend (format, messages, flags, null, cancellationToken, progress); - - Engine.Wait (ic); - - ProcessResponseCodes (ic, this); - - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("APPEND", ic); - - var append = ic.RespCodes.OfType ().FirstOrDefault (); - - if (append != null) - return append.UidSet; - - return new UniqueId[0]; - } - - // FIXME: use an aggregate progress reporter - var uids = new List (); - - for (int i = 0; i < messages.Count; i++) { - var uid = Append (format, messages[i], flags[i], cancellationToken); - if (uids != null && uid.HasValue) - uids.Add (uid.Value); - else - uids = null; - } - - if (uids == null) - return new UniqueId[0]; - - return uids; - } - - /// - /// Appends the specified messages to the folder. - /// - /// - /// Appends the specified messages to the folder and returns the UniqueIds assigned to the messages. - /// - /// The UIDs of the appended messages, if available; otherwise an empty array. - /// The formatting options. - /// The list of messages to append to the folder. - /// The message flags to use for each of the messages. - /// The received dates to use for each of the messages. - /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is null. - /// -or- - /// is null. - /// -or- - /// is null. - /// -or- - /// is null. - /// - /// - /// One or more of the is null. - /// -or- - /// The number of messages, flags, and dates do not match. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// Internationalized formatting was requested but has not been enabled. - /// - /// - /// The does not exist. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// Internationalized formatting was requested but is not supported by the server. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The server replied with a NO or BAD response. - /// - public override IList Append (FormatOptions options, IList messages, IList flags, IList dates, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) - { - if (options == null) - throw new ArgumentNullException (nameof (options)); - - if (messages == null) - throw new ArgumentNullException (nameof (messages)); - - for (int i = 0; i < messages.Count; i++) { - if (messages[i] == null) - throw new ArgumentException ("One or more of the messages is null."); - } - - if (flags == null) - throw new ArgumentNullException (nameof (flags)); - - if (dates == null) - throw new ArgumentNullException (nameof (dates)); - - if (messages.Count != flags.Count || messages.Count != dates.Count) - throw new ArgumentException ("The number of messages, the number of flags, and the number of dates must be equal."); - - CheckState (false, false); - - if (options.International && (Engine.Capabilities & ImapCapabilities.UTF8Accept) == 0) - throw new NotSupportedException ("The IMAP server does not support the UTF8 extension."); - - var format = options.Clone (); - format.NewLineFormat = NewLineFormat.Dos; - - if ((Engine.Capabilities & ImapCapabilities.UTF8Only) == ImapCapabilities.UTF8Only) - format.International = true; - - if (format.International && !Engine.UTF8Enabled) - throw new InvalidOperationException ("The UTF8 extension has not been enabled."); - - if (messages.Count == 0) - return new UniqueId[0]; - - if ((Engine.Capabilities & ImapCapabilities.MultiAppend) != 0) { - var ic = QueueMultiAppend (format, messages, flags, dates, cancellationToken, progress); - - Engine.Wait (ic); - - ProcessResponseCodes (ic, null); - - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("APPEND", ic); - - var append = ic.RespCodes.OfType ().FirstOrDefault (); - - if (append != null) - return append.UidSet; - - return new UniqueId[0]; - } - - // FIXME: use an aggregate progress reporter - var uids = new List (); - - for (int i = 0; i < messages.Count; i++) { - var uid = Append (format, messages[i], flags[i], dates[i], cancellationToken); - if (uids != null && uid.HasValue) - uids.Add (uid.Value); - else - uids = null; - } - - if (uids == null) - return new UniqueId[0]; - - return uids; - } - - /// - /// Copies the specified messages to the destination folder. - /// - /// - /// Copies the specified messages to the destination folder. - /// - /// The UID mapping of the messages in the destination folder, if available; otherwise an empty mapping. - /// The UIDs of the messages to copy. - /// The destination folder. - /// The cancellation token. - /// - /// is null. - /// -or- - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// The destination folder does not belong to the . - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// does not exist. - /// - /// - /// The is not currently open. - /// - /// - /// The IMAP server does not support the UIDPLUS extension. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The server replied with a NO or BAD response. - /// - public override UniqueIdMap CopyTo (IList uids, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)) - { - var set = ImapUtils.FormatUidSet (uids); - - if (destination == null) - throw new ArgumentNullException (nameof (destination)); - - if (!(destination is ImapFolder) || ((ImapFolder) destination).Engine != Engine) - throw new ArgumentException ("The destination folder does not belong to this ImapClient.", nameof (destination)); - - CheckState (true, false); - - if (uids.Count == 0) - return UniqueIdMap.Empty; - - if ((Engine.Capabilities & ImapCapabilities.UidPlus) == 0) { - var indexes = Fetch (uids, MessageSummaryItems.UniqueId, cancellationToken).Select (x => x.Index).ToList (); - CopyTo (indexes, destination, cancellationToken); - return UniqueIdMap.Empty; - } - - var command = string.Format ("UID COPY {0} %F\r\n", set); - var ic = Engine.QueueCommand (cancellationToken, this, command, destination); - - Engine.Wait (ic); - - ProcessResponseCodes (ic, destination); - - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("COPY", ic); - - var copy = ic.RespCodes.OfType ().FirstOrDefault (); - - if (copy != null) - return new UniqueIdMap (copy.SrcUidSet, copy.DestUidSet); - - return UniqueIdMap.Empty; - } - - /// - /// Move the specified messages to the destination folder. - /// - /// - /// Moves the specified messages to the destination folder. - /// If the IMAP server supports the MOVE extension (check the - /// property for the flag), then this operation will be atomic. - /// Otherwise, MailKit implements this by first copying the messages to the destination folder, then - /// marking them for deletion in the originating folder, and finally expunging them (see - /// for more information about how a - /// subset of messages are expunged). Since the server could disconnect at any point between those 3 - /// (or more) commands, it is advisable for clients to implement their own logic for moving messages when - /// the IMAP server does not support the MOVE command in order to better handle spontanious server - /// disconnects and other error conditions. - /// - /// The UID mapping of the messages in the destination folder, if available; otherwise an empty mapping. - /// The UIDs of the messages to move. - /// The destination folder. - /// The cancellation token. - /// - /// is null. - /// -or- - /// is null. - /// - /// - /// is empty. - /// -or- - /// One or more of the is invalid. - /// -or- - /// The destination folder does not belong to the . - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// does not exist. - /// - /// - /// The is not currently open in read-write mode. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The server replied with a NO or BAD response. - /// - public override UniqueIdMap MoveTo (IList uids, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)) - { - if ((Engine.Capabilities & ImapCapabilities.Move) == 0) { - var copied = CopyTo (uids, destination, cancellationToken); - AddFlags (uids, MessageFlags.Deleted, true, cancellationToken); - Expunge (uids, cancellationToken); - return copied; - } - - if ((Engine.Capabilities & ImapCapabilities.UidPlus) == 0) { - var indexes = Fetch (uids, MessageSummaryItems.UniqueId, cancellationToken).Select (x => x.Index).ToList (); - MoveTo (indexes, destination, cancellationToken); - Expunge (uids, cancellationToken); - return UniqueIdMap.Empty; - } - - var set = ImapUtils.FormatUidSet (uids); - - if (destination == null) - throw new ArgumentNullException (nameof (destination)); - - if (!(destination is ImapFolder) || ((ImapFolder) destination).Engine != Engine) - throw new ArgumentException ("The destination folder does not belong to this ImapClient.", nameof (destination)); - - CheckState (true, true); - - if (uids.Count == 0) - return UniqueIdMap.Empty; - - var command = string.Format ("UID MOVE {0} %F\r\n", set); - var ic = Engine.QueueCommand (cancellationToken, this, command, destination); - - Engine.Wait (ic); - - ProcessResponseCodes (ic, destination); - - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("MOVE", ic); - - var copy = ic.RespCodes.OfType ().FirstOrDefault (); - - if (copy != null) - return new UniqueIdMap (copy.SrcUidSet, copy.DestUidSet); - - return UniqueIdMap.Empty; - } - - /// - /// Copies the specified messages to the destination folder. - /// - /// - /// Copies the specified messages to the destination folder. - /// - /// The indexes of the messages to copy. - /// The destination folder. - /// The cancellation token. - /// - /// is null. - /// -or- - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// The destination folder does not belong to the . - /// - /// - /// The has been disposed. - /// - /// - /// The is not currently open. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// does not exist. - /// - /// - /// The is not currently open. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The server replied with a NO or BAD response. - /// - public override void CopyTo (IList indexes, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)) - { - var set = ImapUtils.FormatIndexSet (indexes); - - if (destination == null) - throw new ArgumentNullException (nameof (destination)); - - if (!(destination is ImapFolder) || ((ImapFolder) destination).Engine != Engine) - throw new ArgumentException ("The destination folder does not belong to this ImapClient.", nameof (destination)); - - CheckState (true, false); - - if (indexes.Count == 0) - return; - - var command = string.Format ("COPY {0} %F\r\n", set); - var ic = Engine.QueueCommand (cancellationToken, this, command, destination); - - Engine.Wait (ic); - - ProcessResponseCodes (ic, destination); - - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("COPY", ic); - } - - /// - /// Moves the specified messages to the destination folder. - /// - /// - /// If the IMAP server supports the MOVE command, then the MOVE command will be used. Otherwise, - /// the messages will first be copied to the destination folder and then marked as \Deleted in the - /// originating folder. Since the server could disconnect at any point between those 2 operations, it - /// may be advisable to implement your own logic for moving messages in this case in order to better - /// handle spontanious server disconnects and other error conditions. - /// - /// The indexes of the messages to move. - /// The destination folder. - /// The cancellation token. - /// - /// is null. - /// -or- - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// The destination folder does not belong to the . - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// does not exist. - /// - /// - /// The is not currently open in read-write mode. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The server replied with a NO or BAD response. - /// - public override void MoveTo (IList indexes, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)) - { - if ((Engine.Capabilities & ImapCapabilities.Move) == 0) { - CopyTo (indexes, destination, cancellationToken); - AddFlags (indexes, MessageFlags.Deleted, true, cancellationToken); - return; - } - - var set = ImapUtils.FormatIndexSet (indexes); - - if (destination == null) - throw new ArgumentNullException (nameof (destination)); - - if (!(destination is ImapFolder) || ((ImapFolder) destination).Engine != Engine) - throw new ArgumentException ("The destination folder does not belong to this ImapClient.", nameof (destination)); - - CheckState (true, true); - - if (indexes.Count == 0) - return; - - var command = string.Format ("MOVE {0} %F\r\n", set); - var ic = Engine.QueueCommand (cancellationToken, this, command, destination); - - Engine.Wait (ic); - - ProcessResponseCodes (ic, destination); - - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("MOVE", ic); - } - - static void ReadLiteralData (ImapEngine engine, CancellationToken cancellationToken) - { - var buf = new byte[4096]; - int nread; - - do { - nread = engine.Stream.Read (buf, 0, buf.Length, cancellationToken); - } while (nread > 0); - } - - class FetchSummaryContext - { - public readonly SortedDictionary Results; - public readonly MessageSummaryItems RequestedItems; - - public FetchSummaryContext (MessageSummaryItems requestedItems) - { - Results = new SortedDictionary (); - RequestedItems = requestedItems; - } - } - - void FetchSummaryItems (ImapEngine engine, ImapCommand ic, int index) - { - var token = engine.ReadToken (ic.CancellationToken); - - if (token.Type != ImapTokenType.OpenParen) - throw ImapEngine.UnexpectedToken (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "FETCH", token); - - var ctx = (FetchSummaryContext) ic.UserData; - IMessageSummary isummary; - MessageSummary summary; - - if (!ctx.Results.TryGetValue (index, out isummary)) { - summary = new MessageSummary (index); - ctx.Results.Add (index, summary); - } else { - summary = (MessageSummary) isummary; - } - - do { - token = engine.ReadToken (ic.CancellationToken); - - if (token.Type == ImapTokenType.CloseParen || token.Type == ImapTokenType.Eoln) - break; - - if (token.Type != ImapTokenType.Atom) - throw ImapEngine.UnexpectedToken (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "FETCH", token); - - var atom = (string) token.Value; - string format; - ulong value64; - uint value; - int idx; - - switch (atom) { - case "INTERNALDATE": - token = engine.ReadToken (ic.CancellationToken); - - switch (token.Type) { - case ImapTokenType.QString: - case ImapTokenType.Atom: - summary.InternalDate = ImapUtils.ParseInternalDate ((string) token.Value); - break; - case ImapTokenType.Nil: - summary.InternalDate = null; - break; - default: - throw ImapEngine.UnexpectedToken (ImapEngine.GenericItemSyntaxErrorFormat, atom, token); - } - - summary.Fields |= MessageSummaryItems.InternalDate; - break; - case "RFC822.SIZE": - token = engine.ReadToken (ic.CancellationToken); - - if (token.Type != ImapTokenType.Atom || !uint.TryParse ((string) token.Value, out value)) - throw ImapEngine.UnexpectedToken (ImapEngine.GenericItemSyntaxErrorFormat, atom, token); - - summary.Fields |= MessageSummaryItems.Size; - summary.Size = value; - break; - case "BODYSTRUCTURE": - format = string.Format (ImapEngine.GenericItemSyntaxErrorFormat, "BODYSTRUCTURE", "{0}"); - summary.Body = ImapUtils.ParseBody (engine, format, string.Empty, ic.CancellationToken); - summary.Fields |= MessageSummaryItems.BodyStructure; - break; - case "BODY": - token = engine.PeekToken (ic.CancellationToken); - - if (token.Type == ImapTokenType.OpenBracket) { - // consume the '[' - token = engine.ReadToken (ic.CancellationToken); - - if (token.Type != ImapTokenType.OpenBracket) - throw ImapEngine.UnexpectedToken (ImapEngine.GenericItemSyntaxErrorFormat, atom, token); - - // References and/or other headers were requested... - - do { - token = engine.ReadToken (ic.CancellationToken); - - if (token.Type == ImapTokenType.CloseBracket) - break; - - if (token.Type == ImapTokenType.OpenParen) { - do { - token = engine.ReadToken (ic.CancellationToken); - - if (token.Type == ImapTokenType.CloseParen) - break; - - // the header field names will generally be atoms or qstrings but may also be literals - switch (token.Type) { - case ImapTokenType.Literal: - engine.ReadLiteral (ic.CancellationToken); - break; - case ImapTokenType.QString: - case ImapTokenType.Atom: - break; - default: - throw ImapEngine.UnexpectedToken (ImapEngine.GenericItemSyntaxErrorFormat, atom, token); - } - } while (true); - } else if (token.Type != ImapTokenType.Atom) { - throw ImapEngine.UnexpectedToken (ImapEngine.GenericItemSyntaxErrorFormat, atom, token); - } - } while (true); - - if (token.Type != ImapTokenType.CloseBracket) - throw ImapEngine.UnexpectedToken (ImapEngine.GenericItemSyntaxErrorFormat, atom, token); - - token = engine.ReadToken (ic.CancellationToken); - - if (token.Type != ImapTokenType.Literal) - throw ImapEngine.UnexpectedToken (ImapEngine.GenericItemSyntaxErrorFormat, atom, token); - - summary.References = new MessageIdList (); - - try { - summary.Headers = engine.ParseHeaders (engine.Stream, ic.CancellationToken); - } catch (FormatException) { - // consume any remaining literal data... - ReadLiteralData (engine, ic.CancellationToken); - summary.Headers = new HeaderList (); - } - - if ((idx = summary.Headers.IndexOf (HeaderId.References)) != -1) { - var references = summary.Headers[idx]; - var rawValue = references.RawValue; - - foreach (var msgid in MimeUtils.EnumerateReferences (rawValue, 0, rawValue.Length)) - summary.References.Add (msgid); - } - - summary.Fields |= MessageSummaryItems.References; - } else { - summary.Fields |= MessageSummaryItems.Body; - - try { - format = string.Format (ImapEngine.GenericItemSyntaxErrorFormat, "BODY", "{0}"); - summary.Body = ImapUtils.ParseBody (engine, format, string.Empty, ic.CancellationToken); - } catch (ImapProtocolException ex) { - if (!ex.UnexpectedToken) - throw; - - // Note: GMail's IMAP implementation sometimes replies with completely broken BODY values - // (see issue #32 for the `BODY ("ALTERNATIVE")` example), so to work around this nonsense, - // we need to drop the remainder of this line. - do { - token = engine.PeekToken (ic.CancellationToken); - - if (token.Type == ImapTokenType.Eoln) - break; - - token = engine.ReadToken (ic.CancellationToken); - - if (token.Type == ImapTokenType.Literal) - ReadLiteralData (engine, ic.CancellationToken); - } while (true); - - return; - } - } - break; - case "ENVELOPE": - summary.Envelope = ImapUtils.ParseEnvelope (engine, ic.CancellationToken); - summary.Fields |= MessageSummaryItems.Envelope; - break; - case "FLAGS": - summary.Flags = ImapUtils.ParseFlagsList (engine, atom, summary.UserFlags, ic.CancellationToken); - summary.Fields |= MessageSummaryItems.Flags; - break; - case "MODSEQ": - token = engine.ReadToken (ic.CancellationToken); - - if (token.Type != ImapTokenType.OpenParen) - throw ImapEngine.UnexpectedToken (ImapEngine.GenericItemSyntaxErrorFormat, atom, token); - - token = engine.ReadToken (ic.CancellationToken); - - if (token.Type != ImapTokenType.Atom || !ulong.TryParse ((string) token.Value, out value64)) - throw ImapEngine.UnexpectedToken (ImapEngine.GenericItemSyntaxErrorFormat, atom, token); - - token = engine.ReadToken (ic.CancellationToken); - - if (token.Type != ImapTokenType.CloseParen) - throw ImapEngine.UnexpectedToken (ImapEngine.GenericItemSyntaxErrorFormat, atom, token); - - summary.Fields |= MessageSummaryItems.ModSeq; - summary.ModSeq = value64; - break; - case "UID": - token = engine.ReadToken (ic.CancellationToken); - - if (token.Type != ImapTokenType.Atom || !uint.TryParse ((string) token.Value, out value) || value == 0) - throw ImapEngine.UnexpectedToken (ImapEngine.GenericItemSyntaxErrorFormat, atom, token); - - summary.UniqueId = new UniqueId (ic.Folder.UidValidity, value); - summary.Fields |= MessageSummaryItems.UniqueId; - break; - case "X-GM-MSGID": - token = engine.ReadToken (ic.CancellationToken); - - if (token.Type != ImapTokenType.Atom || !ulong.TryParse ((string) token.Value, out value64) || value64 == 0) - throw ImapEngine.UnexpectedToken (ImapEngine.GenericItemSyntaxErrorFormat, atom, token); - - summary.Fields |= MessageSummaryItems.GMailMessageId; - summary.GMailMessageId = value64; - break; - case "X-GM-THRID": - token = engine.ReadToken (ic.CancellationToken); - - if (token.Type != ImapTokenType.Atom || !ulong.TryParse ((string) token.Value, out value64) || value64 == 0) - throw ImapEngine.UnexpectedToken (ImapEngine.GenericItemSyntaxErrorFormat, atom, token); - - summary.Fields |= MessageSummaryItems.GMailThreadId; - summary.GMailThreadId = value64; - break; - case "X-GM-LABELS": - summary.GMailLabels = ImapUtils.ParseLabelsList (engine, ic.CancellationToken); - summary.Fields |= MessageSummaryItems.GMailLabels; - break; - default: - throw ImapEngine.UnexpectedToken (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "FETCH", token); - } - } while (true); - - if (token.Type != ImapTokenType.CloseParen) - throw ImapEngine.UnexpectedToken (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "FETCH", token); - - if ((ctx.RequestedItems & summary.Fields) == ctx.RequestedItems) - OnMessageSummaryFetched (summary); - } - - static HashSet GetHeaderNames (HashSet fields) - { - if (fields == null) - return null; - - var names = new HashSet (); - - foreach (var field in fields) { - if (field == HeaderId.Unknown) - continue; - - names.Add (field.ToHeaderName ()); - } - - return names; - } - - string FormatSummaryItems (ref MessageSummaryItems items, HashSet fields) - { - if ((items & MessageSummaryItems.BodyStructure) != 0 && (items & MessageSummaryItems.Body) != 0) { - // don't query both the BODY and BODYSTRUCTURE, that's just dumb... - items &= ~MessageSummaryItems.Body; - } - - if (!Engine.IsGMail) { - // first, eliminate the aliases... - if (items == MessageSummaryItems.All) - return "ALL"; - - if (items == MessageSummaryItems.Full) - return "FULL"; - - if (items == MessageSummaryItems.Fast) - return "FAST"; - } - - var tokens = new List (); - - // now add on any additional summary items... - if ((items & MessageSummaryItems.UniqueId) != 0) - tokens.Add ("UID"); - if ((items & MessageSummaryItems.Flags) != 0) - tokens.Add ("FLAGS"); - if ((items & MessageSummaryItems.InternalDate) != 0) - tokens.Add ("INTERNALDATE"); - if ((items & MessageSummaryItems.Size) != 0) - tokens.Add ("RFC822.SIZE"); - if ((items & MessageSummaryItems.Envelope) != 0) - tokens.Add ("ENVELOPE"); - if ((items & MessageSummaryItems.BodyStructure) != 0) - tokens.Add ("BODYSTRUCTURE"); - if ((items & MessageSummaryItems.Body) != 0) - tokens.Add ("BODY"); - - if ((Engine.Capabilities & ImapCapabilities.CondStore) != 0) { - if ((items & MessageSummaryItems.ModSeq) != 0) - tokens.Add ("MODSEQ"); - } - - if ((Engine.Capabilities & ImapCapabilities.GMailExt1) != 0) { - // now for the GMail extension items - if ((items & MessageSummaryItems.GMailMessageId) != 0) - tokens.Add ("X-GM-MSGID"); - if ((items & MessageSummaryItems.GMailThreadId) != 0) - tokens.Add ("X-GM-THRID"); - if ((items & MessageSummaryItems.GMailLabels) != 0) - tokens.Add ("X-GM-LABELS"); - } - - if ((items & MessageSummaryItems.References) != 0 || fields != null) { - var headers = new StringBuilder ("BODY.PEEK[HEADER.FIELDS ("); - bool references = false; - - if (fields != null) { - foreach (var field in fields) { - var name = field.ToUpperInvariant (); - - if (name == "REFERENCES") - references = true; - - headers.Append (name); - headers.Append (' '); - } - } - - if ((items & MessageSummaryItems.References) != 0 && !references) - headers.Append ("REFERENCES "); - - headers[headers.Length - 1] = ')'; - headers.Append (']'); - - tokens.Add (headers.ToString ()); - } - - if (tokens.Count == 1) - return tokens[0]; - - return string.Format ("({0})", string.Join (" ", tokens)); - } - - static IList AsReadOnly (ICollection collection) - { - var array = new IMessageSummary[collection.Count]; - - collection.CopyTo (array, 0); - - return new ReadOnlyCollection (array); - } - - /// - /// Fetches the message summaries for the specified message UIDs. - /// - /// - /// Fetches the message summaries for the specified message UIDs. - /// It should be noted that if another client has modified any message - /// in the folder, the IMAP server may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. - /// - /// - /// - /// - /// An enumeration of summaries for the requested messages. - /// The UIDs. - /// The message summary items to fetch. - /// The cancellation token. - /// - /// is null. - /// - /// - /// is empty. - /// - /// - /// One or more of the is invalid. - /// - /// - /// The has been disposed. - /// - /// - /// The is not currently open. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The server replied with a NO or BAD response. - /// - public override IList Fetch (IList uids, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)) - { - var set = ImapUtils.FormatUidSet (uids); - - if (items == MessageSummaryItems.None) - throw new ArgumentOutOfRangeException (nameof (items)); - - CheckState (true, false); - - if (uids.Count == 0) - return new IMessageSummary[0]; - - var query = FormatSummaryItems (ref items, null); - var command = string.Format ("UID FETCH {0} {1}\r\n", set, query); - var ic = new ImapCommand (Engine, cancellationToken, this, command); - var ctx = new FetchSummaryContext (items); - - ic.RegisterUntaggedHandler ("FETCH", FetchSummaryItems); - ic.UserData = ctx; - - Engine.QueueCommand (ic); - Engine.Wait (ic); - - ProcessResponseCodes (ic, null); - - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("FETCH", ic); - - return AsReadOnly (ctx.Results.Values); - } - - /// - /// Fetches the message summaries for the specified message UIDs. - /// - /// - /// Fetches the message summaries for the specified message UIDs. - /// It should be noted that if another client has modified any message - /// in the folder, the IMAP server may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. - /// - /// An enumeration of summaries for the requested messages. - /// The UIDs. - /// The message summary items to fetch. - /// The desired header fields. - /// The cancellation token. - /// - /// is null. - /// -or- - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// is empty. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The is not currently open. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The server replied with a NO or BAD response. - /// - public override IList Fetch (IList uids, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)) - { - return Fetch (uids, items, GetHeaderNames (fields), cancellationToken); - } - - /// - /// Fetches the message summaries for the specified message UIDs. - /// - /// - /// Fetches the message summaries for the specified message UIDs. - /// It should be noted that if another client has modified any message - /// in the folder, the IMAP server may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. - /// - /// An enumeration of summaries for the requested messages. - /// The UIDs. - /// The message summary items to fetch. - /// The desired header fields. - /// The cancellation token. - /// - /// is null. - /// -or- - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// is empty. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The is not currently open. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The server replied with a NO or BAD response. - /// - public override IList Fetch (IList uids, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)) - { - var set = ImapUtils.FormatUidSet (uids); - - if (fields == null) - throw new ArgumentNullException (nameof (fields)); - - if (fields.Count == 0) - throw new ArgumentException ("The set of header fields cannot be empty.", nameof (fields)); - - CheckState (true, false); - - if (uids.Count == 0) - return new IMessageSummary[0]; - - var query = FormatSummaryItems (ref items, fields); - var command = string.Format ("UID FETCH {0} {1}\r\n", set, query); - var ic = new ImapCommand (Engine, cancellationToken, this, command); - var ctx = new FetchSummaryContext (items); - - ic.RegisterUntaggedHandler ("FETCH", FetchSummaryItems); - ic.UserData = ctx; - - Engine.QueueCommand (ic); - Engine.Wait (ic); - - ProcessResponseCodes (ic, null); - - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("FETCH", ic); - - return AsReadOnly (ctx.Results.Values); - } - - /// - /// Fetches the message summaries for the specified message UIDs that have a - /// higher mod-sequence value than the one specified. - /// - /// - /// Fetches the message summaries for the specified message UIDs that - /// have a higher mod-sequence value than the one specified. - /// If the IMAP server supports the QRESYNC extension and the application has - /// enabled this feature via , - /// then this method will emit events for messages - /// that have vanished since the specified mod-sequence value. - /// It should be noted that if another client has modified any message - /// in the folder, the IMAP server may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. - /// - /// An enumeration of summaries for the requested messages. - /// The UIDs. - /// The mod-sequence value. - /// The message summary items to fetch. - /// The cancellation token. - /// - /// is null. - /// - /// - /// is empty. - /// - /// - /// One or more of the is invalid. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The is not currently open. - /// - /// - /// The does not support mod-sequences. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The server replied with a NO or BAD response. - /// - public override IList Fetch (IList uids, ulong modseq, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)) - { - var set = ImapUtils.FormatUidSet (uids); - - if (items == MessageSummaryItems.None) - throw new ArgumentOutOfRangeException (nameof (items)); - - if (!SupportsModSeq) - throw new NotSupportedException ("The ImapFolder does not support mod-sequences."); - - CheckState (true, false); - - if (uids.Count == 0) - return new IMessageSummary[0]; - - var query = FormatSummaryItems (ref items, null); - var vanished = Engine.QResyncEnabled ? " VANISHED" : string.Empty; - var command = string.Format ("UID FETCH {0} {1} (CHANGEDSINCE {2}{3})\r\n", set, query, modseq, vanished); - var ic = new ImapCommand (Engine, cancellationToken, this, command); - var ctx = new FetchSummaryContext (items); - - ic.RegisterUntaggedHandler ("FETCH", FetchSummaryItems); - ic.UserData = ctx; - - Engine.QueueCommand (ic); - Engine.Wait (ic); - - ProcessResponseCodes (ic, null); - - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("FETCH", ic); - - return AsReadOnly (ctx.Results.Values); - } - - /// - /// Fetches the message summaries for the specified message UIDs that have a - /// higher mod-sequence value than the one specified. - /// - /// - /// Fetches the message summaries for the specified message UIDs that - /// have a higher mod-sequence value than the one specified. - /// If the IMAP server supports the QRESYNC extension and the application has - /// enabled this feature via , - /// then this method will emit events for messages - /// that have vanished since the specified mod-sequence value. - /// It should be noted that if another client has modified any message - /// in the folder, the IMAP server may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. - /// - /// An enumeration of summaries for the requested messages. - /// The UIDs. - /// The mod-sequence value. - /// The message summary items to fetch. - /// The desired header fields. - /// The cancellation token. - /// - /// is null. - /// -or- - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// is empty. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The is not currently open. - /// - /// - /// The does not support mod-sequences. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The server replied with a NO or BAD response. - /// - public override IList Fetch (IList uids, ulong modseq, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)) - { - return Fetch (uids, modseq, items, GetHeaderNames (fields), cancellationToken); - } - - /// - /// Fetches the message summaries for the specified message UIDs that have a - /// higher mod-sequence value than the one specified. - /// - /// - /// Fetches the message summaries for the specified message UIDs that - /// have a higher mod-sequence value than the one specified. - /// If the IMAP server supports the QRESYNC extension and the application has - /// enabled this feature via , - /// then this method will emit events for messages - /// that have vanished since the specified mod-sequence value. - /// It should be noted that if another client has modified any message - /// in the folder, the IMAP server may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. - /// - /// An enumeration of summaries for the requested messages. - /// The UIDs. - /// The mod-sequence value. - /// The message summary items to fetch. - /// The desired header fields. - /// The cancellation token. - /// - /// is null. - /// -or- - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// is empty. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The is not currently open. - /// - /// - /// The does not support mod-sequences. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The server replied with a NO or BAD response. - /// - public override IList Fetch (IList uids, ulong modseq, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)) - { - var set = ImapUtils.FormatUidSet (uids); - - if (fields == null) - throw new ArgumentNullException (nameof (fields)); - - if (fields.Count == 0) - throw new ArgumentException ("The set of header fields cannot be empty.", nameof (fields)); - - if (!SupportsModSeq) - throw new NotSupportedException ("The ImapFolder does not support mod-sequences."); - - CheckState (true, false); - - if (uids.Count == 0) - return new IMessageSummary[0]; - - var query = FormatSummaryItems (ref items, fields); - var vanished = Engine.QResyncEnabled ? " VANISHED" : string.Empty; - var command = string.Format ("UID FETCH {0} {1} (CHANGEDSINCE {2}{3})\r\n", set, query, modseq, vanished); - var ic = new ImapCommand (Engine, cancellationToken, this, command); - var ctx = new FetchSummaryContext (items); - - ic.RegisterUntaggedHandler ("FETCH", FetchSummaryItems); - ic.UserData = ctx; - - Engine.QueueCommand (ic); - Engine.Wait (ic); - - ProcessResponseCodes (ic, null); - - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("FETCH", ic); - - return AsReadOnly (ctx.Results.Values); - } - - /// - /// Fetches the message summaries for the specified message indexes. - /// - /// - /// Fetches the message summaries for the specified message indexes. - /// It should be noted that if another client has modified any message - /// in the folder, the IMAP server may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. - /// - /// An enumeration of summaries for the requested messages. - /// The indexes. - /// The message summary items to fetch. - /// The cancellation token. - /// - /// is null. - /// - /// - /// is empty. - /// - /// - /// One or more of the is invalid. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The is not currently open. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The server replied with a NO or BAD response. - /// - public override IList Fetch (IList indexes, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)) - { - var set = ImapUtils.FormatIndexSet (indexes); - - if (items == MessageSummaryItems.None) - throw new ArgumentOutOfRangeException (nameof (items)); - - CheckState (true, false); - - if (indexes.Count == 0) - return new IMessageSummary[0]; - - var query = FormatSummaryItems (ref items, null); - var command = string.Format ("FETCH {0} {1}\r\n", set, query); - var ic = new ImapCommand (Engine, cancellationToken, this, command); - var ctx = new FetchSummaryContext (items); - - ic.RegisterUntaggedHandler ("FETCH", FetchSummaryItems); - ic.UserData = ctx; - - Engine.QueueCommand (ic); - Engine.Wait (ic); - - ProcessResponseCodes (ic, null); - - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("FETCH", ic); - - return AsReadOnly (ctx.Results.Values); - } - - /// - /// Fetches the message summaries for the specified message indexes. - /// - /// - /// Fetches the message summaries for the specified message indexes. - /// It should be noted that if another client has modified any message - /// in the folder, the IMAP server may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. - /// - /// An enumeration of summaries for the requested messages. - /// The indexes. - /// The message summary items to fetch. - /// The desired header fields. - /// The cancellation token. - /// - /// is null. - /// -or- - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// is empty. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The is not currently open. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The server replied with a NO or BAD response. - /// - public override IList Fetch (IList indexes, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)) - { - return Fetch (indexes, items, GetHeaderNames (fields), cancellationToken); - } - - /// - /// Fetches the message summaries for the specified message indexes. - /// - /// - /// Fetches the message summaries for the specified message indexes. - /// It should be noted that if another client has modified any message - /// in the folder, the IMAP server may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. - /// - /// An enumeration of summaries for the requested messages. - /// The indexes. - /// The message summary items to fetch. - /// The desired header fields. - /// The cancellation token. - /// - /// is null. - /// -or- - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// is empty. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The is not currently open. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The server replied with a NO or BAD response. - /// - public override IList Fetch (IList indexes, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)) - { - var set = ImapUtils.FormatIndexSet (indexes); - - if (fields == null) - throw new ArgumentNullException (nameof (fields)); - - if (fields.Count == 0) - throw new ArgumentException ("The set of header fields cannot be empty.", nameof (fields)); - - CheckState (true, false); - - if (indexes.Count == 0) - return new IMessageSummary[0]; - - var query = FormatSummaryItems (ref items, fields); - var command = string.Format ("FETCH {0} {1}\r\n", set, query); - var ic = new ImapCommand (Engine, cancellationToken, this, command); - var ctx = new FetchSummaryContext (items); - - ic.RegisterUntaggedHandler ("FETCH", FetchSummaryItems); - ic.UserData = ctx; - - Engine.QueueCommand (ic); - Engine.Wait (ic); - - ProcessResponseCodes (ic, null); - - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("FETCH", ic); - - return AsReadOnly (ctx.Results.Values); - } - - /// - /// Fetches the message summaries for the specified message indexes that have a - /// higher mod-sequence value than the one specified. - /// - /// - /// Fetches the message summaries for the specified message indexes that - /// have a higher mod-sequence value than the one specified. - /// It should be noted that if another client has modified any message - /// in the folder, the IMAP server may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. - /// - /// An enumeration of summaries for the requested messages. - /// The indexes. - /// The mod-sequence value. - /// The message summary items to fetch. - /// The cancellation token. - /// - /// is null. - /// - /// - /// is empty. - /// - /// - /// One or more of the is invalid. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The is not currently open. - /// - /// - /// The does not support mod-sequences. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The server replied with a NO or BAD response. - /// - public override IList Fetch (IList indexes, ulong modseq, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)) - { - var set = ImapUtils.FormatIndexSet (indexes); - - if (items == MessageSummaryItems.None) - throw new ArgumentOutOfRangeException (nameof (items)); - - if (!SupportsModSeq) - throw new NotSupportedException ("The ImapFolder does not support mod-sequences."); - - CheckState (true, false); - - if (indexes.Count == 0) - return new IMessageSummary[0]; - - var query = FormatSummaryItems (ref items, null); - var command = string.Format ("FETCH {0} {1} (CHANGEDSINCE {2})\r\n", set, query, modseq); - var ic = new ImapCommand (Engine, cancellationToken, this, command); - var ctx = new FetchSummaryContext (items); - - ic.RegisterUntaggedHandler ("FETCH", FetchSummaryItems); - ic.UserData = ctx; - - Engine.QueueCommand (ic); - Engine.Wait (ic); - - ProcessResponseCodes (ic, null); - - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("FETCH", ic); - - return AsReadOnly (ctx.Results.Values); - } - - /// - /// Fetches the message summaries for the specified message indexes that have a - /// higher mod-sequence value than the one specified. - /// - /// - /// Fetches the message summaries for the specified message indexes that - /// have a higher mod-sequence value than the one specified. - /// It should be noted that if another client has modified any message - /// in the folder, the IMAP server may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. - /// - /// An enumeration of summaries for the requested messages. - /// The indexes. - /// The mod-sequence value. - /// The message summary items to fetch. - /// The desired header fields. - /// The cancellation token. - /// - /// is null. - /// -or- - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// is empty. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The is not currently open. - /// - /// - /// The does not support mod-sequences. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The server replied with a NO or BAD response. - /// - public override IList Fetch (IList indexes, ulong modseq, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)) - { - return Fetch (indexes, modseq, items, GetHeaderNames (fields), cancellationToken); - } - - /// - /// Fetches the message summaries for the specified message indexes that have a - /// higher mod-sequence value than the one specified. - /// - /// - /// Fetches the message summaries for the specified message indexes that - /// have a higher mod-sequence value than the one specified. - /// It should be noted that if another client has modified any message - /// in the folder, the IMAP server may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. - /// - /// An enumeration of summaries for the requested messages. - /// The indexes. - /// The mod-sequence value. - /// The message summary items to fetch. - /// The desired header fields. - /// The cancellation token. - /// - /// is null. - /// -or- - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// is empty. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The is not currently open. - /// - /// - /// The does not support mod-sequences. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The server replied with a NO or BAD response. - /// - public override IList Fetch (IList indexes, ulong modseq, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)) - { - var set = ImapUtils.FormatIndexSet (indexes); - - if (fields == null) - throw new ArgumentNullException (nameof (fields)); - - if (fields.Count == 0) - throw new ArgumentException ("The set of header fields cannot be empty.", nameof (fields)); - - if (!SupportsModSeq) - throw new NotSupportedException ("The ImapFolder does not support mod-sequences."); - - CheckState (true, false); - - if (indexes.Count == 0) - return new IMessageSummary[0]; - - var query = FormatSummaryItems (ref items, fields); - var command = string.Format ("FETCH {0} {1} (CHANGEDSINCE {2})\r\n", set, query, modseq); - var ic = new ImapCommand (Engine, cancellationToken, this, command); - var ctx = new FetchSummaryContext (items); - - ic.RegisterUntaggedHandler ("FETCH", FetchSummaryItems); - ic.UserData = ctx; - - Engine.QueueCommand (ic); - Engine.Wait (ic); - - ProcessResponseCodes (ic, null); - - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("FETCH", ic); - - return AsReadOnly (ctx.Results.Values); - } - - static string GetFetchRange (int min, int max) - { - if (min == max) - return (min + 1).ToString (); - - var maxValue = max != -1 ? (max + 1).ToString () : "*"; - - return string.Format ("{0}:{1}", min + 1, maxValue); - } - - /// - /// Fetches the message summaries for the messages between the two indexes, inclusive. - /// - /// - /// Fetches the message summaries for the messages between the two - /// indexes, inclusive. - /// It should be noted that if another client has modified any message - /// in the folder, the IMAP server may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. - /// - /// An enumeration of summaries for the requested messages. - /// The minimum index. - /// The maximum index, or -1 to specify no upper bound. - /// The message summary items to fetch. - /// The cancellation token. - /// - /// is out of range. - /// -or- - /// is out of range. - /// -or- - /// is empty. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The is not currently open. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The server replied with a NO or BAD response. - /// - public override IList Fetch (int min, int max, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)) - { - if (min < 0) - throw new ArgumentOutOfRangeException (nameof (min)); - - if (max != -1 && max < min) - throw new ArgumentOutOfRangeException (nameof (max)); - - if (items == MessageSummaryItems.None) - throw new ArgumentOutOfRangeException (nameof (items)); - - CheckState (true, false); - - if (min == Count) - return new IMessageSummary[0]; - - var query = FormatSummaryItems (ref items, null); - var command = string.Format ("FETCH {0} {1}\r\n", GetFetchRange (min, max), query); - var ic = new ImapCommand (Engine, cancellationToken, this, command); - var ctx = new FetchSummaryContext (items); - - ic.RegisterUntaggedHandler ("FETCH", FetchSummaryItems); - ic.UserData = ctx; - - Engine.QueueCommand (ic); - Engine.Wait (ic); - - ProcessResponseCodes (ic, null); - - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("FETCH", ic); - - return AsReadOnly (ctx.Results.Values); - } - - /// - /// Fetches the message summaries for the messages between the two indexes, inclusive. - /// - /// - /// Fetches the message summaries for the messages between the two - /// indexes, inclusive. - /// It should be noted that if another client has modified any message - /// in the folder, the IMAP server may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. - /// - /// An enumeration of summaries for the requested messages. - /// The minimum index. - /// The maximum index, or -1 to specify no upper bound. - /// The message summary items to fetch. - /// The desired header fields. - /// The cancellation token. - /// - /// is out of range. - /// -or- - /// is out of range. - /// - /// - /// is null. - /// - /// - /// is empty. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The is not currently open. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The server replied with a NO or BAD response. - /// - public override IList Fetch (int min, int max, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)) - { - return Fetch (min, max, items, GetHeaderNames (fields), cancellationToken); - } - - /// - /// Fetches the message summaries for the messages between the two indexes, inclusive. - /// - /// - /// Fetches the message summaries for the messages between the two - /// indexes, inclusive. - /// It should be noted that if another client has modified any message - /// in the folder, the IMAP server may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. - /// - /// An enumeration of summaries for the requested messages. - /// The minimum index. - /// The maximum index, or -1 to specify no upper bound. - /// The message summary items to fetch. - /// The desired header fields. - /// The cancellation token. - /// - /// is out of range. - /// -or- - /// is out of range. - /// - /// - /// is null. - /// - /// - /// is empty. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The is not currently open. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The server replied with a NO or BAD response. - /// - public override IList Fetch (int min, int max, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)) - { - if (min < 0) - throw new ArgumentOutOfRangeException (nameof (min)); - - if (max != -1 && max < min) - throw new ArgumentOutOfRangeException (nameof (max)); - - if (fields == null) - throw new ArgumentNullException (nameof (fields)); - - if (fields.Count == 0) - throw new ArgumentException ("The set of header fields cannot be empty.", nameof (fields)); - - CheckState (true, false); - - if (min == Count) - return new IMessageSummary[0]; - - var query = FormatSummaryItems (ref items, fields); - var command = string.Format ("FETCH {0} {1}\r\n", GetFetchRange (min, max), query); - var ic = new ImapCommand (Engine, cancellationToken, this, command); - var ctx = new FetchSummaryContext (items); - - ic.RegisterUntaggedHandler ("FETCH", FetchSummaryItems); - ic.UserData = ctx; - - Engine.QueueCommand (ic); - Engine.Wait (ic); - - ProcessResponseCodes (ic, null); - - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("FETCH", ic); - - return AsReadOnly (ctx.Results.Values); - } - - /// - /// Fetches the message summaries for the messages between the two indexes (inclusive) - /// that have a higher mod-sequence value than the one specified. - /// - /// - /// Fetches the message summaries for the messages between the two - /// indexes (inclusive) that have a higher mod-sequence value than the one - /// specified. - /// It should be noted that if another client has modified any message - /// in the folder, the IMAP server may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. - /// - /// An enumeration of summaries for the requested messages. - /// The minimum index. - /// The maximum index, or -1 to specify no upper bound. - /// The mod-sequence value. - /// The message summary items to fetch. - /// The cancellation token. - /// - /// is out of range. - /// -or- - /// is out of range. - /// -or- - /// is empty. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The is not currently open. - /// - /// - /// The does not support mod-sequences. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The server replied with a NO or BAD response. - /// - public override IList Fetch (int min, int max, ulong modseq, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)) - { - if (min < 0) - throw new ArgumentOutOfRangeException (nameof (min)); - - if (max != -1 && max < min) - throw new ArgumentOutOfRangeException (nameof (max)); - - if (items == MessageSummaryItems.None) - throw new ArgumentOutOfRangeException (nameof (items)); - - if (!SupportsModSeq) - throw new NotSupportedException ("The ImapFolder does not support mod-sequences."); - - CheckState (true, false); - - var query = FormatSummaryItems (ref items, null); - var command = string.Format ("FETCH {0} {1} (CHANGEDSINCE {2})\r\n", GetFetchRange (min, max), query, modseq); - var ic = new ImapCommand (Engine, cancellationToken, this, command); - var ctx = new FetchSummaryContext (items); - - ic.RegisterUntaggedHandler ("FETCH", FetchSummaryItems); - ic.UserData = ctx; - - Engine.QueueCommand (ic); - Engine.Wait (ic); - - ProcessResponseCodes (ic, null); - - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("FETCH", ic); - - return AsReadOnly (ctx.Results.Values); - } - - /// - /// Fetches the message summaries for the messages between the two indexes (inclusive) - /// that have a higher mod-sequence value than the one specified. - /// - /// - /// Fetches the message summaries for the messages between the two - /// indexes (inclusive) that have a higher mod-sequence value than the one - /// specified. - /// It should be noted that if another client has modified any message - /// in the folder, the IMAP server may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. - /// - /// An enumeration of summaries for the requested messages. - /// The minimum index. - /// The maximum index, or -1 to specify no upper bound. - /// The mod-sequence value. - /// The message summary items to fetch. - /// The desired header fields. - /// The cancellation token. - /// - /// is out of range. - /// -or- - /// is out of range. - /// - /// - /// is null. - /// - /// - /// is empty. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The is not currently open. - /// - /// - /// The does not support mod-sequences. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The server replied with a NO or BAD response. - /// - public override IList Fetch (int min, int max, ulong modseq, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)) - { - return Fetch (min, max, modseq, items, GetHeaderNames (fields), cancellationToken); - } - - /// - /// Fetches the message summaries for the messages between the two indexes (inclusive) - /// that have a higher mod-sequence value than the one specified. - /// - /// - /// Fetches the message summaries for the messages between the two - /// indexes (inclusive) that have a higher mod-sequence value than the one - /// specified. - /// It should be noted that if another client has modified any message - /// in the folder, the IMAP server may choose to return information that was - /// not explicitly requested. It is therefore important to be prepared to - /// handle both additional fields on a for - /// messages that were requested as well as summaries for messages that were - /// not requested at all. - /// - /// An enumeration of summaries for the requested messages. - /// The minimum index. - /// The maximum index, or -1 to specify no upper bound. - /// The mod-sequence value. - /// The message summary items to fetch. - /// The desired header fields. - /// The cancellation token. - /// - /// is out of range. - /// -or- - /// is out of range. - /// - /// - /// is null. - /// - /// - /// is empty. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The is not currently open. - /// - /// - /// The does not support mod-sequences. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The server replied with a NO or BAD response. - /// - public override IList Fetch (int min, int max, ulong modseq, MessageSummaryItems items, HashSet fields, CancellationToken cancellationToken = default (CancellationToken)) - { - if (min < 0) - throw new ArgumentOutOfRangeException (nameof (min)); - - if (max != -1 && max < min) - throw new ArgumentOutOfRangeException (nameof (max)); - - if (fields == null) - throw new ArgumentNullException (nameof (fields)); - - if (fields.Count == 0) - throw new ArgumentException ("The set of header fields cannot be empty.", nameof (fields)); - - if (!SupportsModSeq) - throw new NotSupportedException ("The ImapFolder does not support mod-sequences."); - - CheckState (true, false); - - var query = FormatSummaryItems (ref items, fields); - var command = string.Format ("FETCH {0} {1} (CHANGEDSINCE {2})\r\n", GetFetchRange (min, max), query, modseq); - var ic = new ImapCommand (Engine, cancellationToken, this, command); - var ctx = new FetchSummaryContext (items); - - ic.RegisterUntaggedHandler ("FETCH", FetchSummaryItems); - ic.UserData = ctx; - - Engine.QueueCommand (ic); - Engine.Wait (ic); - - ProcessResponseCodes (ic, null); - - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("FETCH", ic); - - return AsReadOnly (ctx.Results.Values); - } - - /// - /// Create a backing stream for use with the GetMessage, GetBodyPart, and GetStream methods. - /// - /// - /// Allows subclass implementations to override the type of stream - /// created for use with the GetMessage, GetBodyPart and GetStream methods. - /// This could be useful for subclass implementations that intend to implement - /// support for caching and/or for subclass implementations that want to use - /// temporary file streams instead of memory-based streams for larger amounts of - /// message data. - /// Subclasses that implement caching using this API should wait for - /// before adding the stream to their cache. - /// Streams returned by this method SHOULD clean up any allocated resources - /// such as deleting temporary files from the file system. - /// The will not be available for the various - /// GetMessage(), GetBodyPart() and GetStream() methods that take a message index rather - /// than a . It may also not be available if the IMAP server - /// response does not specify the UID value prior to sending the literal-string - /// token containing the message stream. - /// - /// - /// The stream. - /// The unique identifier of the message, if available. - /// The section of the message that is being fetched. - /// The starting offset of the message section being fetched. - /// The length of the stream being fetched, measured in bytes. - protected virtual Stream CreateStream (UniqueId? uid, string section, int offset, int length) - { - if (length > 4096) - return new MemoryBlockStream (); - - return new MemoryStream (length); - } - - /// - /// Commit a stream returned by . - /// - /// - /// Commits a stream returned by . - /// This method is called only after both the message data has successfully - /// been written to the stream returned by and a - /// has been obtained for the associated message. - /// For subclasses implementing caching, this method should be used for - /// committing the stream to their cache. - /// Subclass implementations may take advantage of the fact that - /// allows returning a new - /// reference if they move a file on the file system and wish to return a new - /// based on the new path, for example. - /// - /// - /// The stream. - /// The stream. - /// The unique identifier of the message. - protected virtual Stream CommitStream (Stream stream, UniqueId uid) - { - return stream; - } - - HeaderList ParseHeaders (Stream stream, CancellationToken cancellationToken) - { - try { - return Engine.ParseHeaders (stream, cancellationToken); - } finally { - stream.Dispose (); - } - } - - MimeMessage ParseMessage (Stream stream, CancellationToken cancellationToken) - { - bool dispose = !(stream is MemoryStream || stream is MemoryBlockStream); - - try { - return Engine.ParseMessage (stream, !dispose, cancellationToken); - } finally { - if (dispose) - stream.Dispose (); - } - } - - MimeEntity ParseEntity (Stream stream, bool dispose, CancellationToken cancellationToken) - { - try { - return Engine.ParseEntity (stream, !dispose, cancellationToken); - } finally { - if (dispose) - stream.Dispose (); - } - } - - class FetchStreamContext : IDisposable - { - public readonly Dictionary Sections = new Dictionary (StringComparer.OrdinalIgnoreCase); - readonly ITransferProgress Progress; - - public FetchStreamContext (ITransferProgress progress) - { - Progress = progress; - } - - public void Report (long nread, long total) - { - if (Progress == null) - return; - - Progress.Report (nread, total); - } - - public void Dispose () - { - foreach (var section in Sections) { - try { - section.Value.Dispose (); - } catch (IOException) { - } - } - } - } - - void FetchStream (ImapEngine engine, ImapCommand ic, int index) - { - var token = engine.ReadToken (ic.CancellationToken); - var labels = new MessageLabelsChangedEventArgs (index); - var flags = new MessageFlagsChangedEventArgs (index); - var modSeq = new ModSeqChangedEventArgs (index); - var ctx = (FetchStreamContext) ic.UserData; - var section = new StringBuilder (); - bool modSeqChanged = false; - bool labelsChanged = false; - bool flagsChanged = false; - var buf = new byte[4096]; - long nread = 0, size = 0; - UniqueId? uid = null; - Stream stream; - int n; - - if (token.Type != ImapTokenType.OpenParen) - throw ImapEngine.UnexpectedToken (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "FETCH", token); - - do { - token = engine.ReadToken (ic.CancellationToken); - - if (token.Type == ImapTokenType.CloseParen || token.Type == ImapTokenType.Eoln) - break; - - if (token.Type != ImapTokenType.Atom) - throw ImapEngine.UnexpectedToken (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "FETCH", token); - - var atom = (string) token.Value; - int offset = 0, length; - ulong modseq; - uint value; - - switch (atom) { - case "BODY": - token = engine.ReadToken (ic.CancellationToken); - - if (token.Type != ImapTokenType.OpenBracket) - throw ImapEngine.UnexpectedToken (ImapEngine.GenericItemSyntaxErrorFormat, atom, token); - - section.Clear (); - - do { - token = engine.ReadToken (ic.CancellationToken); - - if (token.Type == ImapTokenType.CloseBracket) - break; - - if (token.Type == ImapTokenType.OpenParen) { - section.Append (" ("); - - do { - token = engine.ReadToken (ic.CancellationToken); - - if (token.Type == ImapTokenType.CloseParen) - break; - - // the header field names will generally be atoms or qstrings but may also be literals - switch (token.Type) { - case ImapTokenType.Literal: - section.Append (engine.ReadLiteral (ic.CancellationToken)); - break; - case ImapTokenType.QString: - case ImapTokenType.Atom: - section.Append ((string) token.Value); - break; - default: - throw ImapEngine.UnexpectedToken (ImapEngine.GenericItemSyntaxErrorFormat, atom, token); - } - - section.Append (' '); - } while (true); - - if (section[section.Length - 1] == ' ') - section.Length--; - - section.Append (')'); - } else if (token.Type != ImapTokenType.Atom) { - throw ImapEngine.UnexpectedToken (ImapEngine.GenericItemSyntaxErrorFormat, atom, token); - } else { - section.Append ((string) token.Value); - } - } while (true); - - if (token.Type != ImapTokenType.CloseBracket) - throw ImapEngine.UnexpectedToken (ImapEngine.GenericItemSyntaxErrorFormat, atom, token); - - token = engine.ReadToken (ic.CancellationToken); - - if (token.Type == ImapTokenType.Atom) { - // this might be a region ("<###>") - var expr = (string) token.Value; - - if (expr.Length > 2 && expr[0] == '<' && expr[expr.Length - 1] == '>') { - var region = expr.Substring (1, expr.Length - 2); - int.TryParse (region, out offset); - - token = engine.ReadToken (ic.CancellationToken); - } - } - - switch (token.Type) { - case ImapTokenType.Literal: - length = (int) token.Value; - size += length; - - stream = CreateStream (uid, section.ToString (), offset, length); - - try { - while ((n = engine.Stream.Read (buf, 0, buf.Length, ic.CancellationToken)) > 0) { - stream.Write (buf, 0, n); - nread += n; - - ctx.Report (nread, size); - } - - stream.Position = 0; - } catch { - stream.Dispose (); - throw; - } - break; - case ImapTokenType.QString: - case ImapTokenType.Atom: - var buffer = Encoding.UTF8.GetBytes ((string) token.Value); - length = buffer.Length; - nread += length; - size += length; - - stream = CreateStream (uid, section.ToString (), offset, length); - - try { - stream.Write (buffer, 0, length); - ctx.Report (nread, size); - stream.Position = 0; - } catch { - stream.Dispose (); - throw; - } - break; - case ImapTokenType.Nil: - stream = CreateStream (uid, section.ToString (), offset, 0); - break; - default: - throw ImapEngine.UnexpectedToken (ImapEngine.GenericItemSyntaxErrorFormat, atom, token); - } - - if (uid.HasValue) - ctx.Sections[section.ToString ()] = CommitStream (stream, uid.Value); - else - ctx.Sections[section.ToString ()] = stream; - - break; - case "UID": - token = engine.ReadToken (ic.CancellationToken); - - if (token.Type != ImapTokenType.Atom || !uint.TryParse ((string) token.Value, out value) || value == 0) - throw ImapEngine.UnexpectedToken (ImapEngine.GenericItemSyntaxErrorFormat, atom, token); - - uid = new UniqueId (UidValidity, value); - - foreach (var key in ctx.Sections.Keys.ToArray ()) - ctx.Sections[key] = CommitStream (ctx.Sections[key], uid.Value); - - labels.UniqueId = uid.Value; - flags.UniqueId = uid.Value; - break; - case "MODSEQ": - token = engine.ReadToken (ic.CancellationToken); - - if (token.Type != ImapTokenType.OpenParen) - throw ImapEngine.UnexpectedToken (ImapEngine.GenericItemSyntaxErrorFormat, atom, token); - - token = engine.ReadToken (ic.CancellationToken); - - if (token.Type != ImapTokenType.Atom || !ulong.TryParse ((string) token.Value, out modseq)) - throw ImapEngine.UnexpectedToken (ImapEngine.GenericItemSyntaxErrorFormat, atom, token); - - token = engine.ReadToken (ic.CancellationToken); - - if (token.Type != ImapTokenType.CloseParen) - throw ImapEngine.UnexpectedToken (ImapEngine.GenericItemSyntaxErrorFormat, atom, token); - - if (modseq > HighestModSeq) - UpdateHighestModSeq (modseq); - - modSeq.ModSeq = modseq; - labels.ModSeq = modseq; - flags.ModSeq = modseq; - modSeqChanged = true; - break; - case "FLAGS": - // even though we didn't request this piece of information, the IMAP server - // may send it if another client has recently modified the message flags. - flags.Flags = ImapUtils.ParseFlagsList (engine, atom, flags.UserFlags, ic.CancellationToken); - flagsChanged = true; - break; - case "X-GM-LABELS": - // even though we didn't request this piece of information, the IMAP server - // may send it if another client has recently modified the message labels. - labels.Labels = ImapUtils.ParseLabelsList (engine, ic.CancellationToken); - labelsChanged = true; - break; - default: - throw ImapEngine.UnexpectedToken (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "FETCH", token); - } - } while (true); - - if (token.Type != ImapTokenType.CloseParen) - throw ImapEngine.UnexpectedToken (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "FETCH", token); - - if (flagsChanged) - OnMessageFlagsChanged (flags); - - if (labelsChanged) - OnMessageLabelsChanged (labels); - - if (modSeqChanged) - OnModSeqChanged (modSeq); - } - - /// - /// Gets the specified message headers. - /// - /// - /// Gets the specified message headers. - /// - /// The message headers. - /// The UID of the message. - /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is invalid. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The is not currently open. - /// - /// - /// The IMAP server did not return the requested message headers. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The server replied with a NO or BAD response. - /// - public override HeaderList GetHeaders (UniqueId uid, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) - { - if (!uid.IsValid) - throw new ArgumentException ("The uid is invalid.", nameof (uid)); - - CheckState (true, false); - - var ic = new ImapCommand (Engine, cancellationToken, this, "UID FETCH %u (BODY.PEEK[HEADER])\r\n", uid.Id); - var ctx = new FetchStreamContext (progress); - Stream stream; - - ic.RegisterUntaggedHandler ("FETCH", FetchStream); - ic.UserData = ctx; - - Engine.QueueCommand (ic); - - try { - Engine.Wait (ic); - - ProcessResponseCodes (ic, null); - - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("FETCH", ic); - - if (!ctx.Sections.TryGetValue ("HEADER", out stream)) - throw new MessageNotFoundException ("The IMAP server did not return the requested message headers."); - - ctx.Sections.Remove ("HEADER"); - } finally { - ctx.Dispose (); - } - - return ParseHeaders (stream, cancellationToken); - } - - /// - /// Gets the specified body part headers. - /// - /// - /// Gets the specified body part headers. - /// - /// The body part headers. - /// The UID of the message. - /// The body part specifier. - /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is null. - /// - /// - /// is invalid. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The is not currently open. - /// - /// - /// The IMAP server did not return the requested body part headers. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The server replied with a NO or BAD response. - /// - public HeaderList GetHeaders (UniqueId uid, string partSpecifier, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) - { - if (!uid.IsValid) - throw new ArgumentException ("The uid is invalid.", nameof (uid)); - - if (partSpecifier == null) - throw new ArgumentNullException (nameof (partSpecifier)); - - CheckState (true, false); - - string[] tags; - - var command = string.Format ("UID FETCH {0} ({1})\r\n", uid.Id, GetBodyPartQuery (partSpecifier, true, out tags)); - var ic = new ImapCommand (Engine, cancellationToken, this, command); - var ctx = new FetchStreamContext (progress); - Stream stream; - - ic.RegisterUntaggedHandler ("FETCH", FetchStream); - ic.UserData = ctx; - - Engine.QueueCommand (ic); - - try { - Engine.Wait (ic); - - ProcessResponseCodes (ic, null); - - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("FETCH", ic); - - if (!ctx.Sections.TryGetValue (tags[0], out stream)) - throw new MessageNotFoundException ("The IMAP server did not return the requested body part headers."); - - ctx.Sections.Remove (tags[0]); - } finally { - ctx.Dispose (); - } - - return ParseHeaders (stream, cancellationToken); - } - - /// - /// Gets the specified body part headers. - /// - /// - /// Gets the specified body part headers. - /// - /// The body part headers. - /// The UID of the message. - /// The body part. - /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is null. - /// - /// - /// is invalid. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The is not currently open. - /// - /// - /// The IMAP server did not return the requested body part headers. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The server replied with a NO or BAD response. - /// - public override HeaderList GetHeaders (UniqueId uid, BodyPart part, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) - { - if (!uid.IsValid) - throw new ArgumentException ("The uid is invalid.", nameof (uid)); - - if (part == null) - throw new ArgumentNullException (nameof (part)); - - return GetHeaders (uid, part.PartSpecifier, cancellationToken, progress); - } - - /// - /// Gets the specified message headers. - /// - /// - /// Gets the specified message headers. - /// - /// The message headers. - /// The index of the message. - /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is out of range. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The is not currently open. - /// - /// - /// The IMAP server did not return the requested message headers. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The server replied with a NO or BAD response. - /// - public override HeaderList GetHeaders (int index, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) - { - if (index < 0 || index >= Count) - throw new ArgumentOutOfRangeException (nameof (index)); - - CheckState (true, false); - - var ic = new ImapCommand (Engine, cancellationToken, this, "FETCH %d (BODY.PEEK[HEADER])\r\n", index + 1); - var ctx = new FetchStreamContext (progress); - Stream stream; - - ic.RegisterUntaggedHandler ("FETCH", FetchStream); - ic.UserData = ctx; - - Engine.QueueCommand (ic); - - try { - Engine.Wait (ic); - - ProcessResponseCodes (ic, null); - - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("FETCH", ic); - - if (!ctx.Sections.TryGetValue ("HEADER", out stream)) - throw new MessageNotFoundException ("The IMAP server did not return the requested message."); - - ctx.Sections.Remove ("HEADER"); - } finally { - ctx.Dispose (); - } - - return ParseHeaders (stream, cancellationToken); - } - - /// - /// Gets the specified body part headers. - /// - /// - /// Gets the specified body part headers. + /// Gets the specified subfolder. /// - /// The body part headers. - /// The index of the message. - /// The body part specifier. + /// The subfolder. + /// The name of the subfolder. /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is out of range. - /// /// - /// is null. + /// is . + /// + /// + /// is either an empty string or contains the . /// /// /// The has been disposed. @@ -5698,97 +1984,78 @@ void FetchStream (ImapEngine engine, ImapCommand ic, int index) /// /// The is not authenticated. /// - /// - /// The is not currently open. - /// - /// - /// The IMAP server did not return the requested body part headers. - /// /// /// The operation was canceled via the cancellation token. /// /// /// An I/O error occurred. /// + /// + /// The requested folder could not be found. + /// /// /// The server's response contained unexpected tokens. /// /// /// The server replied with a NO or BAD response. /// - public HeaderList GetHeaders (int index, string partSpecifier, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + public override async Task GetSubfolderAsync (string name, CancellationToken cancellationToken = default) { - if (index < 0 || index >= Count) - throw new ArgumentOutOfRangeException (nameof (index)); - - if (partSpecifier == null) - throw new ArgumentNullException (nameof (partSpecifier)); - - CheckState (true, false); - - string[] tags; + if (!TryQueueGetSubfolderCommand (name, cancellationToken, out var ic, out var list, out var fullName, out var encodedName, out var folder)) + return folder ?? throw new FolderNotFoundException (name); - var command = string.Format ("FETCH {0} ({1})\r\n", index + 1, GetBodyPartQuery (partSpecifier, true, out tags)); - var ic = new ImapCommand (Engine, cancellationToken, this, command); - var ctx = new FetchStreamContext (progress); - Stream stream; + await Engine.RunAsync (ic).ConfigureAwait (false); - ic.RegisterUntaggedHandler ("FETCH", FetchStream); - ic.UserData = ctx; + folder = ProcessGetSubfolderResponse (ic, list, encodedName); - Engine.QueueCommand (ic); + if (list.Count > 1 || folder == null) { + // Note: if any folders returned in the LIST command are unparented, have the ImapEngine look up their + // parent folders now so that they are not left in an inconsistent state. + await Engine.LookupParentFoldersAsync (list, cancellationToken).ConfigureAwait (false); + } - try { - Engine.Wait (ic); + if (folder == null) + throw new FolderNotFoundException (fullName); - ProcessResponseCodes (ic, null); + return folder; + } - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("FETCH", ic); + ImapCommand QueueCheckCommand (CancellationToken cancellationToken) + { + CheckState (true, false); - if (!ctx.Sections.TryGetValue (tags[0], out stream)) - throw new MessageNotFoundException ("The IMAP server did not return the requested body part headers."); + return Engine.QueueCommand (cancellationToken, this, "CHECK\r\n"); + } - ctx.Sections.Remove (tags[0]); - } finally { - ctx.Dispose (); - } + void ProcessCheckResponse (ImapCommand ic) + { + ProcessResponseCodes (ic, null); - return ParseHeaders (stream, cancellationToken); + ic.ThrowIfNotOk ("CHECK"); } /// - /// Gets the specified body part headers. + /// Force the server to sync its in-memory state with its disk state. /// /// - /// Gets the specified body part headers. + /// The CHECK command forces the IMAP server to sync its + /// in-memory state with its disk state. + /// For more information about the CHECK command, see + /// rfc350101. /// - /// The body part headers. - /// The index of the message. - /// The body part. /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is out of range. - /// - /// - /// is null. - /// /// /// The has been disposed. /// + /// + /// The is not currently open. + /// /// /// The is not connected. /// /// /// The is not authenticated. /// - /// - /// The is not currently open. - /// - /// - /// The IMAP server did not return the requested body part headers. - /// /// /// The operation was canceled via the cancellation token. /// @@ -5801,45 +2068,38 @@ void FetchStream (ImapEngine engine, ImapCommand ic, int index) /// /// The server replied with a NO or BAD response. /// - public override HeaderList GetHeaders (int index, BodyPart part, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + public override void Check (CancellationToken cancellationToken = default) { - if (index < 0 || index >= Count) - throw new ArgumentOutOfRangeException (nameof (index)); + var ic = QueueCheckCommand (cancellationToken); - if (part == null) - throw new ArgumentNullException (nameof (part)); + Engine.Run (ic); - return GetHeaders (index, part.PartSpecifier, cancellationToken, progress); + ProcessCheckResponse (ic); } /// - /// Gets the specified message. + /// Asynchronously force the server to sync its in-memory state with its disk state. /// /// - /// Gets the specified message. + /// The CHECK command forces the IMAP server to sync its + /// in-memory state with its disk state. + /// For more information about the CHECK command, see + /// rfc350101. /// - /// The message. - /// The UID of the message. + /// An awaitable task. /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is invalid. - /// /// /// The has been disposed. /// + /// + /// The is not currently open. + /// /// /// The is not connected. /// /// /// The is not authenticated. /// - /// - /// The is not currently open. - /// - /// - /// The IMAP server did not return the requested message. - /// /// /// The operation was canceled via the cancellation token. /// @@ -5852,167 +2112,78 @@ void FetchStream (ImapEngine engine, ImapCommand ic, int index) /// /// The server replied with a NO or BAD response. /// - public override MimeMessage GetMessage (UniqueId uid, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + public override async Task CheckAsync (CancellationToken cancellationToken = default) { - if (!uid.IsValid) - throw new ArgumentException ("The uid is invalid.", nameof (uid)); - - CheckState (true, false); - - var ic = new ImapCommand (Engine, cancellationToken, this, "UID FETCH %u (BODY.PEEK[])\r\n", uid.Id); - var ctx = new FetchStreamContext (progress); - Stream stream; - - ic.RegisterUntaggedHandler ("FETCH", FetchStream); - ic.UserData = ctx; - - Engine.QueueCommand (ic); - - try { - Engine.Wait (ic); - - ProcessResponseCodes (ic, null); - - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("FETCH", ic); - - if (!ctx.Sections.TryGetValue (string.Empty, out stream)) - throw new MessageNotFoundException ("The IMAP server did not return the requested message."); + var ic = QueueCheckCommand (cancellationToken); - ctx.Sections.Remove (string.Empty); - } finally { - ctx.Dispose (); - } + await Engine.RunAsync (ic).ConfigureAwait (false); - return ParseMessage (stream, cancellationToken); + ProcessCheckResponse (ic); } - /// - /// Gets the specified message. - /// - /// - /// Gets the specified message. - /// - /// The message. - /// The index of the message. - /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is out of range. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The is not currently open. - /// - /// - /// The IMAP server did not return the requested message. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The server replied with a NO or BAD response. - /// - public override MimeMessage GetMessage (int index, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + ImapCommand? QueueStatusCommand (StatusItems items, CancellationToken cancellationToken) { - if (index < 0 || index >= Count) - throw new ArgumentOutOfRangeException (nameof (index)); + if ((Engine.Capabilities & ImapCapabilities.Status) == 0) + throw new NotSupportedException ("The IMAP server does not support the STATUS command."); - CheckState (true, false); + CheckState (false, false); - var ic = new ImapCommand (Engine, cancellationToken, this, "FETCH %d (BODY.PEEK[])\r\n", index + 1); - var ctx = new FetchStreamContext (progress); - Stream stream; + if (items == StatusItems.None) + return null; - ic.RegisterUntaggedHandler ("FETCH", FetchStream); - ic.UserData = ctx; + var command = string.Format ("STATUS %F ({0})\r\n", Engine.GetStatusQuery (items)); - Engine.QueueCommand (ic); + return Engine.QueueCommand (cancellationToken, null, command, this); + } - try { - Engine.Wait (ic); + void ProcessStatusResponse (ImapCommand ic, bool throwNotFound) + { + ProcessResponseCodes (ic, this, throwNotFound); - ProcessResponseCodes (ic, null); + ic.ThrowIfNotOk ("STATUS"); + } - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("FETCH", ic); + internal void Status (StatusItems items, bool throwNotFound, CancellationToken cancellationToken) + { + var ic = QueueStatusCommand (items, cancellationToken); - if (!ctx.Sections.TryGetValue (string.Empty, out stream)) - throw new MessageNotFoundException ("The IMAP server did not return the requested message."); + if (ic == null) + return; - ctx.Sections.Remove (string.Empty); - } finally { - ctx.Dispose (); - } + Engine.Run (ic); - return ParseMessage (stream, cancellationToken); + ProcessStatusResponse (ic, throwNotFound); } - static string GetBodyPartQuery (string partSpec, bool headersOnly, out string[] tags) + internal async Task StatusAsync (StatusItems items, bool throwNotFound, CancellationToken cancellationToken) { - string query; + var ic = QueueStatusCommand (items, cancellationToken); - if (headersOnly) { - tags = new string[1]; - - if (partSpec.Length > 0) { - query = string.Format ("BODY.PEEK[{0}.MIME]", partSpec); - tags[0] = partSpec + ".MIME"; - } else { - query = "BODY.PEEK[HEADER]"; - tags[0] = "HEADER"; - } - } else { - tags = new string[2]; - - if (partSpec.Length > 0) { - tags[0] = partSpec + ".MIME"; - tags[1] = partSpec; - } else { - tags[0] = "HEADER"; - tags[1] = "TEXT"; - } + if (ic == null) + return; - query = string.Format ("BODY.PEEK[{0}] BODY.PEEK[{1}]", tags[0], tags[1]); - } + await Engine.RunAsync (ic).ConfigureAwait (false); - return query; + ProcessStatusResponse (ic, throwNotFound); } /// - /// Gets the specified body part. + /// Update the values of the specified items. /// /// - /// Gets the specified body part. + /// Updates the values of the specified items. + /// The method + /// MUST NOT be used on a folder that is already in the opened state. Instead, other ways + /// of getting the desired information should be used. + /// For example, a common use for the + /// method is to get the number of unread messages in the folder. When the folder is open, however, it is + /// possible to use the + /// method to query for the list of unread messages. + /// For more information about the STATUS command, see + /// rfc3501. /// - /// - /// - /// - /// The body part. - /// The UID of the message. - /// The body part. + /// The items to update. /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is null. - /// - /// - /// is invalid. - /// /// /// The has been disposed. /// @@ -6022,11 +2193,11 @@ static string GetBodyPartQuery (string partSpec, bool headersOnly, out string[] /// /// The is not authenticated. /// - /// - /// The is not currently open. + /// + /// The does not exist. /// - /// - /// The IMAP server did not return the requested message body. + /// + /// The IMAP server does not support the STATUS command. /// /// /// The operation was canceled via the cancellation token. @@ -6040,35 +2211,29 @@ static string GetBodyPartQuery (string partSpec, bool headersOnly, out string[] /// /// The server replied with a NO or BAD response. /// - public override MimeEntity GetBodyPart (UniqueId uid, BodyPart part, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + public override void Status (StatusItems items, CancellationToken cancellationToken = default) { - if (!uid.IsValid) - throw new ArgumentException ("The uid is invalid.", nameof (uid)); - - if (part == null) - throw new ArgumentNullException (nameof (part)); - - return GetBodyPart (uid, part.PartSpecifier, false, cancellationToken, progress); + Status (items, true, cancellationToken); } /// - /// Gets the specified body part. + /// Asynchronously update the values of the specified items. /// /// - /// Gets the specified body part. + /// Updates the values of the specified items. + /// The method + /// MUST NOT be used on a folder that is already in the opened state. Instead, other ways + /// of getting the desired information should be used. + /// For example, a common use for the + /// method is to get the number of unread messages in the folder. When the folder is open, however, it is + /// possible to use the + /// method to query for the list of unread messages. + /// For more information about the STATUS command, see + /// rfc3501. /// - /// The body part. - /// The UID of the message. - /// The body part. - /// true if only the headers should be downloaded; otherwise, false> + /// An awaitable task. + /// The items to update. /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is null. - /// - /// - /// is invalid. - /// /// /// The has been disposed. /// @@ -6078,11 +2243,11 @@ static string GetBodyPartQuery (string partSpec, bool headersOnly, out string[] /// /// The is not authenticated. /// - /// - /// The is not currently open. + /// + /// The does not exist. /// - /// - /// The IMAP server did not return the requested message body. + /// + /// The IMAP server does not support the STATUS command. /// /// /// The operation was canceled via the cancellation token. @@ -6096,35 +2261,94 @@ static string GetBodyPartQuery (string partSpec, bool headersOnly, out string[] /// /// The server replied with a NO or BAD response. /// - [Obsolete ("Use GetBodyPart(UniqueId, BodyPart, CancellationToken, ITransferProgress) or GetHeaders(UniqueId, BodyPart, CancellationToken, ITransferProgress)")] - public override MimeEntity GetBodyPart (UniqueId uid, BodyPart part, bool headersOnly, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + public override Task StatusAsync (StatusItems items, CancellationToken cancellationToken = default) + { + return StatusAsync (items, true, cancellationToken); + } + + static void ParseAcl (ImapEngine engine, ImapCommand ic) + { + string format = string.Format (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ACL", "{0}"); + var acl = (AccessControlList) ic.UserData!; + string name, rights; + ImapToken token; + + // read the mailbox name + ImapUtils.ReadFolderName (engine, format, false, ic.CancellationToken); + + do { + name = ImapUtils.ReadStringToken (engine, format, ic.CancellationToken); + rights = ImapUtils.ReadStringToken (engine, format, ic.CancellationToken); + + acl.Add (new AccessControl (name, rights)); + + token = engine.PeekToken (ic.CancellationToken); + } while (token.Type != ImapTokenType.Eoln); + } + + static async Task ParseAclAsync (ImapEngine engine, ImapCommand ic) + { + string format = string.Format (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ACL", "{0}"); + var acl = (AccessControlList) ic.UserData!; + string name, rights; + ImapToken token; + + // read the mailbox name + await ImapUtils.ReadFolderNameAsync (engine, format, false, ic.CancellationToken).ConfigureAwait (false); + + do { + name = await ImapUtils.ReadStringTokenAsync (engine, format, ic.CancellationToken).ConfigureAwait (false); + rights = await ImapUtils.ReadStringTokenAsync (engine, format, ic.CancellationToken).ConfigureAwait (false); + + acl.Add (new AccessControl (name, rights)); + + token = await engine.PeekTokenAsync (ic.CancellationToken).ConfigureAwait (false); + } while (token.Type != ImapTokenType.Eoln); + } + + static Task UntaggedAclHandler (ImapEngine engine, ImapCommand ic, int index, bool doAsync) + { + if (doAsync) + return ParseAclAsync (engine, ic); + + ParseAcl (engine, ic); + + return Task.CompletedTask; + } + + ImapCommand QueueGetAccessControlListCommand (CancellationToken cancellationToken) + { + if ((Engine.Capabilities & ImapCapabilities.Acl) == 0) + throw new NotSupportedException ("The IMAP server does not support the ACL extension."); + + CheckState (false, false); + + var ic = new ImapCommand (Engine, cancellationToken, null, "GETACL %F\r\n", this); + ic.RegisterUntaggedHandler ("ACL", UntaggedAclHandler); + ic.UserData = new AccessControlList (); + + Engine.QueueCommand (ic); + + return ic; + } + + AccessControlList ProcessGetAccessControlListResponse (ImapCommand ic) { - if (!uid.IsValid) - throw new ArgumentException ("The uid is invalid.", nameof (uid)); + ProcessResponseCodes (ic, null); - if (part == null) - throw new ArgumentNullException (nameof (part)); + ic.ThrowIfNotOk ("GETACL"); - return GetBodyPart (uid, part.PartSpecifier, headersOnly, cancellationToken, progress); + return (AccessControlList) ic.UserData!; } /// - /// Gets the specified body part. + /// Get the complete access control list for the folder. /// /// - /// Gets the specified body part. + /// Gets the complete access control list for the folder. /// - /// The body part. - /// The UID of the message. - /// The body part specifier. + /// The access control list. /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is null. - /// - /// - /// is invalid. - /// /// /// The has been disposed. /// @@ -6134,11 +2358,8 @@ static string GetBodyPartQuery (string partSpec, bool headersOnly, out string[] /// /// The is not authenticated. /// - /// - /// The is not currently open. - /// - /// - /// The IMAP server did not return the requested message body. + /// + /// The IMAP server does not support the ACL extension. /// /// /// The operation was canceled via the cancellation token. @@ -6150,31 +2371,25 @@ static string GetBodyPartQuery (string partSpec, bool headersOnly, out string[] /// The server's response contained unexpected tokens. /// /// - /// The server replied with a NO or BAD response. + /// The command failed. /// - public MimeEntity GetBodyPart (UniqueId uid, string partSpecifier, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + public override AccessControlList GetAccessControlList (CancellationToken cancellationToken = default) { - return GetBodyPart (uid, partSpecifier, false, cancellationToken, progress); + var ic = QueueGetAccessControlListCommand (cancellationToken); + + Engine.Run (ic); + + return ProcessGetAccessControlListResponse (ic); } /// - /// Gets the specified body part. + /// Asynchronously get the complete access control list for the folder. /// /// - /// Gets the specified body part. + /// Gets the complete access control list for the folder. /// - /// The body part. - /// The UID of the message. - /// The body part specifier. - /// true if only the headers should be downloaded; otherwise, false> + /// The access control list. /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is null. - /// - /// - /// is invalid. - /// /// /// The has been disposed. /// @@ -6184,11 +2399,8 @@ static string GetBodyPartQuery (string partSpec, bool headersOnly, out string[] /// /// The is not authenticated. /// - /// - /// The is not currently open. - /// - /// - /// The IMAP server did not return the requested message body. + /// + /// The IMAP server does not support the ACL extension. /// /// /// The operation was canceled via the cancellation token. @@ -6200,88 +2412,108 @@ static string GetBodyPartQuery (string partSpec, bool headersOnly, out string[] /// The server's response contained unexpected tokens. /// /// - /// The server replied with a NO or BAD response. + /// The command failed. /// - public MimeEntity GetBodyPart (UniqueId uid, string partSpecifier, bool headersOnly, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + public override async Task GetAccessControlListAsync (CancellationToken cancellationToken = default) { - if (!uid.IsValid) - throw new ArgumentException ("The uid is invalid.", nameof (uid)); + var ic = QueueGetAccessControlListCommand (cancellationToken); - if (partSpecifier == null) - throw new ArgumentNullException (nameof (partSpecifier)); + await Engine.RunAsync (ic).ConfigureAwait (false); - CheckState (true, false); + return ProcessGetAccessControlListResponse (ic); + } - string[] tags; + static void ParseListRights (ImapEngine engine, ImapCommand ic) + { + string format = string.Format (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "LISTRIGHTS", "{0}"); + var access = (AccessRights) ic.UserData!; + ImapToken token; - var command = string.Format ("UID FETCH {0} ({1})\r\n", uid.Id, GetBodyPartQuery (partSpecifier, headersOnly, out tags)); - var ic = new ImapCommand (Engine, cancellationToken, this, command); - var ctx = new FetchStreamContext (progress); - ChainedStream chained; - bool dispose = false; - Stream stream; + // read the mailbox name + ImapUtils.ReadFolderName (engine, format, false, ic.CancellationToken); - ic.RegisterUntaggedHandler ("FETCH", FetchStream); - ic.UserData = ctx; + // read the identity name + ImapUtils.ReadStringToken (engine, format, ic.CancellationToken); - Engine.QueueCommand (ic); + do { + var rights = ImapUtils.ReadStringToken (engine, format, ic.CancellationToken); - try { - Engine.Wait (ic); + access.AddRange (rights); - ProcessResponseCodes (ic, null); + token = engine.PeekToken (ic.CancellationToken); + } while (token.Type != ImapTokenType.Eoln); + } - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("FETCH", ic); + static async Task ParseListRightsAsync (ImapEngine engine, ImapCommand ic) + { + string format = string.Format (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "LISTRIGHTS", "{0}"); + var access = (AccessRights) ic.UserData!; + ImapToken token; - chained = new ChainedStream (); + // read the mailbox name + await ImapUtils.ReadFolderNameAsync (engine, format, false, ic.CancellationToken).ConfigureAwait (false); - foreach (var tag in tags) { - if (!ctx.Sections.TryGetValue (tag, out stream)) - throw new MessageNotFoundException ("The IMAP server did not return the requested body part."); + // read the identity name + await ImapUtils.ReadStringTokenAsync (engine, format, ic.CancellationToken).ConfigureAwait (false); - if (!(stream is MemoryStream || stream is MemoryBlockStream)) - dispose = true; + do { + var rights = await ImapUtils.ReadStringTokenAsync (engine, format, ic.CancellationToken).ConfigureAwait (false); - chained.Add (stream); - } + access.AddRange (rights); - foreach (var tag in tags) - ctx.Sections.Remove (tag); - } finally { - ctx.Dispose (); - } + token = await engine.PeekTokenAsync (ic.CancellationToken).ConfigureAwait (false); + } while (token.Type != ImapTokenType.Eoln); + } - var entity = ParseEntity (chained, dispose, cancellationToken); + static Task UntaggedListRightsHandler (ImapEngine engine, ImapCommand ic, int index, bool doAsync) + { + if (doAsync) + return ParseListRightsAsync (engine, ic); - if (partSpecifier.Length == 0) { - for (int i = entity.Headers.Count; i > 0; i--) { - var header = entity.Headers[i - 1]; + ParseListRights (engine, ic); - if (!header.Field.StartsWith ("Content-", StringComparison.OrdinalIgnoreCase)) - entity.Headers.RemoveAt (i - 1); - } - } + return Task.CompletedTask; + } + + ImapCommand QueueGetAccessRightsCommand (string name, CancellationToken cancellationToken) + { + if (name == null) + throw new ArgumentNullException (nameof (name)); + + if ((Engine.Capabilities & ImapCapabilities.Acl) == 0) + throw new NotSupportedException ("The IMAP server does not support the ACL extension."); + + CheckState (false, false); + + var ic = new ImapCommand (Engine, cancellationToken, null, "LISTRIGHTS %F %S\r\n", this, name); + ic.RegisterUntaggedHandler ("LISTRIGHTS", UntaggedListRightsHandler); + ic.UserData = new AccessRights (); + + Engine.QueueCommand (ic); + + return ic; + } + + AccessRights ProcessGetAccessRightsResponse (ImapCommand ic) + { + ProcessResponseCodes (ic, null); + + ic.ThrowIfNotOk ("LISTRIGHTS"); - return entity; + return (AccessRights) ic.UserData!; } /// - /// Gets the specified body part. + /// Get the access rights for a particular identifier. /// /// - /// Gets the specified body part. + /// Gets the access rights for a particular identifier. /// - /// The body part. - /// The index of the message. - /// The body part. + /// The access rights. + /// The identifier name. /// The cancellation token. - /// The progress reporting mechanism. /// - /// is null. - /// - /// - /// is out of range. + /// is . /// /// /// The has been disposed. @@ -6292,11 +2524,8 @@ static string GetBodyPartQuery (string partSpec, bool headersOnly, out string[] /// /// The is not authenticated. /// - /// - /// The is not currently open. - /// - /// - /// The IMAP server did not return the requested message. + /// + /// The IMAP server does not support the ACL extension. /// /// /// The operation was canceled via the cancellation token. @@ -6308,36 +2537,28 @@ static string GetBodyPartQuery (string partSpec, bool headersOnly, out string[] /// The server's response contained unexpected tokens. /// /// - /// The server replied with a NO or BAD response. + /// The command failed. /// - public override MimeEntity GetBodyPart (int index, BodyPart part, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + public override AccessRights GetAccessRights (string name, CancellationToken cancellationToken = default) { - if (index< 0 || index >= Count) - throw new ArgumentOutOfRangeException (nameof (index)); + var ic = QueueGetAccessRightsCommand (name, cancellationToken); - if (part == null) - throw new ArgumentNullException (nameof (part)); + Engine.Run (ic); - return GetBodyPart (index, part.PartSpecifier, false, cancellationToken, progress); + return ProcessGetAccessRightsResponse (ic); } /// - /// Gets the specified body part. + /// Asynchronously get the access rights for a particular identifier. /// /// - /// Gets the specified body part. + /// Gets the access rights for a particular identifier. /// - /// The body part. - /// The index of the message. - /// The body part. - /// true if only the headers should be downloaded; otherwise, false> + /// The access rights. + /// The identifier name. /// The cancellation token. - /// The progress reporting mechanism. /// - /// is null. - /// - /// - /// is out of range. + /// is . /// /// /// The has been disposed. @@ -6348,11 +2569,8 @@ static string GetBodyPartQuery (string partSpec, bool headersOnly, out string[] /// /// The is not authenticated. /// - /// - /// The is not currently open. - /// - /// - /// The IMAP server did not return the requested message. + /// + /// The IMAP server does not support the ACL extension. /// /// /// The operation was canceled via the cancellation token. @@ -6364,37 +2582,84 @@ static string GetBodyPartQuery (string partSpec, bool headersOnly, out string[] /// The server's response contained unexpected tokens. /// /// - /// The server replied with a NO or BAD response. + /// The command failed. /// - [Obsolete ("Use GetBodyPart(int, BodyPart, CancellationToken, ITransferProgress) or GetHeaders(int, BodyPart, CancellationToken, ITransferProgress)")] - public override MimeEntity GetBodyPart (int index, BodyPart part, bool headersOnly, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + public override async Task GetAccessRightsAsync (string name, CancellationToken cancellationToken = default) { - if (index < 0 || index >= Count) - throw new ArgumentOutOfRangeException (nameof (index)); + var ic = QueueGetAccessRightsCommand (name, cancellationToken); + + await Engine.RunAsync (ic).ConfigureAwait (false); + + return ProcessGetAccessRightsResponse (ic); + } + + static void ParseMyRights (ImapEngine engine, ImapCommand ic) + { + string format = string.Format (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "MYRIGHTS", "{0}"); + var access = (AccessRights) ic.UserData!; + + // read the mailbox name + ImapUtils.ReadFolderName (engine, format, false, ic.CancellationToken); + + // read the access rights + access.AddRange (ImapUtils.ReadStringToken (engine, format, ic.CancellationToken)); + } + + static async Task ParseMyRightsAsync (ImapEngine engine, ImapCommand ic) + { + string format = string.Format (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "MYRIGHTS", "{0}"); + var access = (AccessRights) ic.UserData!; + + // read the mailbox name + await ImapUtils.ReadFolderNameAsync (engine, format, false, ic.CancellationToken).ConfigureAwait (false); + + // read the access rights + access.AddRange (await ImapUtils.ReadStringTokenAsync (engine, format, ic.CancellationToken).ConfigureAwait (false)); + } + + static Task UntaggedMyRightsHandler (ImapEngine engine, ImapCommand ic, int index, bool doAsync) + { + if (doAsync) + return ParseMyRightsAsync (engine, ic); + + ParseMyRights (engine, ic); + + return Task.CompletedTask; + } + + ImapCommand QueueGetMyAccessRightsCommand (CancellationToken cancellationToken) + { + if ((Engine.Capabilities & ImapCapabilities.Acl) == 0) + throw new NotSupportedException ("The IMAP server does not support the ACL extension."); - if (part == null) - throw new ArgumentNullException (nameof (part)); + CheckState (false, false); + + var ic = new ImapCommand (Engine, cancellationToken, null, "MYRIGHTS %F\r\n", this); + ic.RegisterUntaggedHandler ("MYRIGHTS", UntaggedMyRightsHandler); + ic.UserData = new AccessRights (); + + Engine.QueueCommand (ic); + + return ic; + } + + AccessRights ProcessGetMyAccessRightsResponse (ImapCommand ic) + { + ProcessResponseCodes (ic, null); - return GetBodyPart (index, part.PartSpecifier, headersOnly, cancellationToken, progress); + ic.ThrowIfNotOk ("MYRIGHTS"); + + return (AccessRights) ic.UserData!; } /// - /// Gets the specified body part. + /// Get the access rights for the current authenticated user. /// /// - /// Gets the specified body part. + /// Gets the access rights for the current authenticated user. /// - /// The body part. - /// The index of the message. - /// The body part specifier. + /// The access rights. /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is null. - /// - /// - /// is out of range. - /// /// /// The has been disposed. /// @@ -6404,11 +2669,8 @@ static string GetBodyPartQuery (string partSpec, bool headersOnly, out string[] /// /// The is not authenticated. /// - /// - /// The is not currently open. - /// - /// - /// The IMAP server did not return the requested message. + /// + /// The IMAP server does not support the ACL extension. /// /// /// The operation was canceled via the cancellation token. @@ -6420,31 +2682,25 @@ static string GetBodyPartQuery (string partSpec, bool headersOnly, out string[] /// The server's response contained unexpected tokens. /// /// - /// The server replied with a NO or BAD response. + /// The command failed. /// - public MimeEntity GetBodyPart (int index, string partSpecifier, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + public override AccessRights GetMyAccessRights (CancellationToken cancellationToken = default) { - return GetBodyPart (index, partSpecifier, false, cancellationToken, progress); + var ic = QueueGetMyAccessRightsCommand (cancellationToken); + + Engine.Run (ic); + + return ProcessGetMyAccessRightsResponse (ic); } /// - /// Gets the specified body part. + /// Asynchronously get the access rights for the current authenticated user. /// /// - /// Gets the specified body part. + /// Gets the access rights for the current authenticated user. /// - /// The body part. - /// The index of the message. - /// The body part specifier. - /// true if only the headers should be downloaded; otherwise, false> + /// The access rights. /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is null. - /// - /// - /// is out of range. - /// /// /// The has been disposed. /// @@ -6454,11 +2710,8 @@ static string GetBodyPartQuery (string partSpec, bool headersOnly, out string[] /// /// The is not authenticated. /// - /// - /// The is not currently open. - /// - /// - /// The IMAP server did not return the requested message. + /// + /// The IMAP server does not support the ACL extension. /// /// /// The operation was canceled via the cancellation token. @@ -6470,94 +2723,59 @@ static string GetBodyPartQuery (string partSpec, bool headersOnly, out string[] /// The server's response contained unexpected tokens. /// /// - /// The server replied with a NO or BAD response. + /// The command failed. /// - public MimeEntity GetBodyPart (int index, string partSpecifier, bool headersOnly, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + public override async Task GetMyAccessRightsAsync (CancellationToken cancellationToken = default) { - if (index < 0 || index >= Count) - throw new ArgumentOutOfRangeException (nameof (index)); - - if (partSpecifier == null) - throw new ArgumentNullException (nameof (partSpecifier)); - - CheckState (true, false); - - string[] tags; - - var command = string.Format ("FETCH {0} ({1})\r\n", index + 1, GetBodyPartQuery (partSpecifier, headersOnly, out tags)); - var ic = new ImapCommand (Engine, cancellationToken, this, command); - var ctx = new FetchStreamContext (progress); - ChainedStream chained; - bool dispose = false; - Stream stream; - - ic.RegisterUntaggedHandler ("FETCH", FetchStream); - ic.UserData = ctx; - - Engine.QueueCommand (ic); + var ic = QueueGetMyAccessRightsCommand (cancellationToken); - try { - Engine.Wait (ic); - - ProcessResponseCodes (ic, null); + await Engine.RunAsync (ic).ConfigureAwait (false); - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("FETCH", ic); - - chained = new ChainedStream (); + return ProcessGetMyAccessRightsResponse (ic); + } - foreach (var tag in tags) { - if (!ctx.Sections.TryGetValue (tag, out stream)) - throw new MessageNotFoundException ("The IMAP server did not return the requested body part."); + ImapCommand QueueModifyAccessRightsCommand (string name, string action, AccessRights rights, CancellationToken cancellationToken) + { + if (name == null) + throw new ArgumentNullException (nameof (name)); - if (!(stream is MemoryStream || stream is MemoryBlockStream)) - dispose = true; + if (rights == null) + throw new ArgumentNullException (nameof (rights)); - chained.Add (stream); - } + if (action.Length != 0 && rights.Count == 0) + throw new ArgumentException ("No rights were specified.", nameof (rights)); - foreach (var tag in tags) - ctx.Sections.Remove (tag); - } finally { - ctx.Dispose (); - } + if ((Engine.Capabilities & ImapCapabilities.Acl) == 0) + throw new NotSupportedException ("The IMAP server does not support the ACL extension."); - var entity = ParseEntity (chained, dispose, cancellationToken); + CheckState (false, false); - if (partSpecifier.Length == 0) { - for (int i = entity.Headers.Count; i > 0; i--) { - var header = entity.Headers[i - 1]; + return Engine.QueueCommand (cancellationToken, null, "SETACL %F %S %S\r\n", this, name, action + rights); + } - if (!header.Field.StartsWith ("Content-", StringComparison.OrdinalIgnoreCase)) - entity.Headers.RemoveAt (i - 1); - } - } + void ProcessModifyAccessRightsResponse (ImapCommand ic) + { + ProcessResponseCodes (ic, null); - return entity; + ic.ThrowIfNotOk ("SETACL"); } /// - /// Gets a substream of the specified message. + /// Add access rights for the specified identity. /// /// - /// Fetches a substream of the message. If the starting offset is beyond - /// the end of the message, an empty stream is returned. If the number of - /// bytes desired extends beyond the end of the message, a truncated stream - /// will be returned. + /// Adds the given access rights for the specified identity. /// - /// The stream. - /// The UID of the message. - /// The starting offset of the first desired byte. - /// The number of bytes desired. + /// The identity name. + /// The access rights. /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is invalid. - /// - /// - /// is negative. + /// + /// is . /// -or- - /// is negative. + /// is . + /// + /// + /// No rights were specified. /// /// /// The has been disposed. @@ -6568,11 +2786,8 @@ static string GetBodyPartQuery (string partSpec, bool headersOnly, out string[] /// /// The is not authenticated. /// - /// - /// The is not currently open. - /// - /// - /// The IMAP server did not return the requested message stream. + /// + /// The IMAP server does not support the ACL extension. /// /// /// The operation was canceled via the cancellation token. @@ -6584,73 +2799,34 @@ static string GetBodyPartQuery (string partSpec, bool headersOnly, out string[] /// The server's response contained unexpected tokens. /// /// - /// The server replied with a NO or BAD response. - /// - public override Stream GetStream (UniqueId uid, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) - { - if (!uid.IsValid) - throw new ArgumentException ("The uid is invalid.", nameof (uid)); - - if (offset < 0) - throw new ArgumentOutOfRangeException (nameof (offset)); - - if (count < 0) - throw new ArgumentOutOfRangeException (nameof (count)); - - CheckState (true, false); - - if (count == 0) - return new MemoryStream (); - - var ic = new ImapCommand (Engine, cancellationToken, this, "UID FETCH %u (BODY.PEEK[]<%d.%d>)\r\n", uid.Id, offset, count); - var ctx = new FetchStreamContext (progress); - Stream stream; - - ic.RegisterUntaggedHandler ("FETCH", FetchStream); - ic.UserData = ctx; - - Engine.QueueCommand (ic); - - try { - Engine.Wait (ic); - - ProcessResponseCodes (ic, null); - - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("FETCH", ic); - - if (!ctx.Sections.TryGetValue (string.Empty, out stream)) - throw new MessageNotFoundException ("The IMAP server did not return the requested stream."); + /// The command failed. + /// + public override void AddAccessRights (string name, AccessRights rights, CancellationToken cancellationToken = default) + { + var ic = QueueModifyAccessRightsCommand (name, "+", rights, cancellationToken); - ctx.Sections.Remove (string.Empty); - } finally { - ctx.Dispose (); - } + Engine.Run (ic); - return stream; + ProcessModifyAccessRightsResponse (ic); } /// - /// Gets a substream of the specified message. + /// Asynchronously add access rights for the specified identity. /// /// - /// Fetches a substream of the message. If the starting offset is beyond - /// the end of the message, an empty stream is returned. If the number of - /// bytes desired extends beyond the end of the message, a truncated stream - /// will be returned. + /// Adds the given access rights for the specified identity. /// - /// The stream. - /// The index of the message. - /// The starting offset of the first desired byte. - /// The number of bytes desired. + /// An asynchronous task context. + /// The identity name. + /// The access rights. /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is out of range. - /// -or- - /// is negative. + /// + /// is . /// -or- - /// is negative. + /// is . + /// + /// + /// No rights were specified. /// /// /// The has been disposed. @@ -6661,11 +2837,8 @@ static string GetBodyPartQuery (string partSpec, bool headersOnly, out string[] /// /// The is not authenticated. /// - /// - /// The is not currently open. - /// - /// - /// The IMAP server did not return the requested message stream. + /// + /// The IMAP server does not support the ACL extension. /// /// /// The operation was canceled via the cancellation token. @@ -6677,70 +2850,33 @@ static string GetBodyPartQuery (string partSpec, bool headersOnly, out string[] /// The server's response contained unexpected tokens. /// /// - /// The server replied with a NO or BAD response. + /// The command failed. /// - public override Stream GetStream (int index, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + public override async Task AddAccessRightsAsync (string name, AccessRights rights, CancellationToken cancellationToken = default) { - if (index < 0 || index >= Count) - throw new ArgumentOutOfRangeException (nameof (index)); - - if (offset < 0) - throw new ArgumentOutOfRangeException (nameof (offset)); - - if (count < 0) - throw new ArgumentOutOfRangeException (nameof (count)); - - CheckState (true, false); - - if (count == 0) - return new MemoryStream (); - - var ic = new ImapCommand (Engine, cancellationToken, this, "FETCH %d (BODY.PEEK[]<%d.%d>)\r\n", index + 1, offset, count); - var ctx = new FetchStreamContext (progress); - Stream stream; - - ic.RegisterUntaggedHandler ("FETCH", FetchStream); - ic.UserData = ctx; - - Engine.QueueCommand (ic); + var ic = QueueModifyAccessRightsCommand (name, "+", rights, cancellationToken); - try { - Engine.Wait (ic); - - ProcessResponseCodes (ic, null); - - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("FETCH", ic); - - if (!ctx.Sections.TryGetValue (string.Empty, out stream)) - throw new MessageNotFoundException ("The IMAP server did not return the requested stream."); + await Engine.RunAsync (ic).ConfigureAwait (false); - ctx.Sections.Remove (string.Empty); - } finally { - ctx.Dispose (); - } - - return stream; + ProcessModifyAccessRightsResponse (ic); } /// - /// Gets a substream of the specified body part. + /// Remove access rights for the specified identity. /// /// - /// Gets a substream of the specified message. - /// For more information about how to construct the , - /// see Section 6.4.5 of RFC3501. + /// Removes the given access rights for the specified identity. /// - /// The stream. - /// The UID of the message. - /// The desired section of the message. + /// The identity name. + /// The access rights. /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is invalid. - /// /// - /// is null. + /// is . + /// -or- + /// is . + /// + /// + /// No rights were specified. /// /// /// The has been disposed. @@ -6751,11 +2887,8 @@ static string GetBodyPartQuery (string partSpec, bool headersOnly, out string[] /// /// The is not authenticated. /// - /// - /// The is not currently open. - /// - /// - /// The IMAP server did not return the requested message stream. + /// + /// The IMAP server does not support the ACL extension. /// /// /// The operation was canceled via the cancellation token. @@ -6767,75 +2900,34 @@ static string GetBodyPartQuery (string partSpec, bool headersOnly, out string[] /// The server's response contained unexpected tokens. /// /// - /// The server replied with a NO or BAD response. + /// The command failed. /// - public override Stream GetStream (UniqueId uid, string section, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + public override void RemoveAccessRights (string name, AccessRights rights, CancellationToken cancellationToken = default) { - if (!uid.IsValid) - throw new ArgumentException ("The uid is invalid.", nameof (uid)); - - if (section == null) - throw new ArgumentNullException (nameof (section)); - - CheckState (true, false); - - var command = string.Format ("UID FETCH {0} (BODY.PEEK[{1}])\r\n", uid.Id, section); - var ic = new ImapCommand (Engine, cancellationToken, this, command); - var ctx = new FetchStreamContext (progress); - Stream stream; - - ic.RegisterUntaggedHandler ("FETCH", FetchStream); - ic.UserData = ctx; + var ic = QueueModifyAccessRightsCommand (name, "-", rights, cancellationToken); - Engine.QueueCommand (ic); - - try { - Engine.Wait (ic); - - ProcessResponseCodes (ic, null); - - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("FETCH", ic); - - if (!ctx.Sections.TryGetValue (section, out stream)) - throw new MessageNotFoundException ("The IMAP server did not return the requested stream."); - - ctx.Sections.Remove (section); - } finally { - ctx.Dispose (); - } + Engine.Run (ic); - return stream; + ProcessModifyAccessRightsResponse (ic); } /// - /// Gets a substream of the specified message. + /// Asynchronously remove access rights for the specified identity. /// /// - /// Gets a substream of the specified message. If the starting offset is beyond - /// the end of the specified section of the message, an empty stream is returned. If - /// the number of bytes desired extends beyond the end of the section, a truncated - /// stream will be returned. - /// For more information about how to construct the , - /// see Section 6.4.5 of RFC3501. + /// Removes the given access rights for the specified identity. /// - /// The stream. - /// The UID of the message. - /// The desired section of the message. - /// The starting offset of the first desired byte. - /// The number of bytes desired. + /// An asynchronous task context. + /// The identity name. + /// The access rights. /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is invalid. - /// /// - /// is null. - /// - /// - /// is negative. + /// is . /// -or- - /// is negative. + /// is . + /// + /// + /// No rights were specified. /// /// /// The has been disposed. @@ -6846,11 +2938,8 @@ static string GetBodyPartQuery (string partSpec, bool headersOnly, out string[] /// /// The is not authenticated. /// - /// - /// The is not currently open. - /// - /// - /// The IMAP server did not return the requested message stream. + /// + /// The IMAP server does not support the ACL extension. /// /// /// The operation was canceled via the cancellation token. @@ -6862,74 +2951,30 @@ static string GetBodyPartQuery (string partSpec, bool headersOnly, out string[] /// The server's response contained unexpected tokens. /// /// - /// The server replied with a NO or BAD response. + /// The command failed. /// - public override Stream GetStream (UniqueId uid, string section, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + public override async Task RemoveAccessRightsAsync (string name, AccessRights rights, CancellationToken cancellationToken = default) { - if (!uid.IsValid) - throw new ArgumentException ("The uid is invalid.", nameof (uid)); - - if (section == null) - throw new ArgumentNullException (nameof (section)); - - if (offset < 0) - throw new ArgumentOutOfRangeException (nameof (offset)); - - if (count < 0) - throw new ArgumentOutOfRangeException (nameof (count)); - - CheckState (true, false); + var ic = QueueModifyAccessRightsCommand (name, "-", rights, cancellationToken); - if (count == 0) - return new MemoryStream (); + await Engine.RunAsync (ic).ConfigureAwait (false); - var command = string.Format ("UID FETCH {0} (BODY.PEEK[{1}]<{2}.{3}>)\r\n", uid.Id, section, offset, count); - var ic = new ImapCommand (Engine, cancellationToken, this, command); - var ctx = new FetchStreamContext (progress); - Stream stream; - - ic.RegisterUntaggedHandler ("FETCH", FetchStream); - ic.UserData = ctx; - - Engine.QueueCommand (ic); - - try { - Engine.Wait (ic); - - ProcessResponseCodes (ic, null); - - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("FETCH", ic); - - if (!ctx.Sections.TryGetValue (section, out stream)) - throw new MessageNotFoundException ("The IMAP server did not return the requested stream."); - - ctx.Sections.Remove (section); - } finally { - ctx.Dispose (); - } - - return stream; + ProcessModifyAccessRightsResponse (ic); } /// - /// Gets a substream of the specified message. + /// Set the access rights for the specified identity. /// /// - /// Gets a substream of the specified message. - /// For more information about how to construct the , - /// see Section 6.4.5 of RFC3501. + /// Sets the access rights for the specified identity. /// - /// The stream. - /// The index of the message. - /// The desired section of the message. + /// The identity name. + /// The access rights. /// The cancellation token. - /// The progress reporting mechanism. /// - /// is null. - /// - /// - /// is out of range. + /// is . + /// -or- + /// is . /// /// /// The has been disposed. @@ -6940,11 +2985,8 @@ static string GetBodyPartQuery (string partSpec, bool headersOnly, out string[] /// /// The is not authenticated. /// - /// - /// The is not currently open. - /// - /// - /// The IMAP server did not return the requested message stream. + /// + /// The IMAP server does not support the ACL extension. /// /// /// The operation was canceled via the cancellation token. @@ -6956,74 +2998,31 @@ static string GetBodyPartQuery (string partSpec, bool headersOnly, out string[] /// The server's response contained unexpected tokens. /// /// - /// The server replied with a NO or BAD response. + /// The command failed. /// - public override Stream GetStream (int index, string section, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + public override void SetAccessRights (string name, AccessRights rights, CancellationToken cancellationToken = default) { - if (index < 0 || index >= Count) - throw new ArgumentOutOfRangeException (nameof (index)); - - if (section == null) - throw new ArgumentNullException (nameof (section)); - - CheckState (true, false); - - var command = string.Format ("FETCH {0} (BODY.PEEK[{1}])\r\n", index + 1, section); - var ic = new ImapCommand (Engine, cancellationToken, this, command); - var ctx = new FetchStreamContext (progress); - Stream stream; - - ic.RegisterUntaggedHandler ("FETCH", FetchStream); - ic.UserData = ctx; - - Engine.QueueCommand (ic); - - try { - Engine.Wait (ic); - - ProcessResponseCodes (ic, null); - - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("FETCH", ic); - - if (!ctx.Sections.TryGetValue (section, out stream)) - throw new MessageNotFoundException ("The IMAP server did not return the requested stream."); + var ic = QueueModifyAccessRightsCommand (name, string.Empty, rights, cancellationToken); - ctx.Sections.Remove (section); - } finally { - ctx.Dispose (); - } + Engine.Run (ic); - return stream; + ProcessModifyAccessRightsResponse (ic); } /// - /// Gets a substream of the specified message. + /// Asynchronously get the access rights for the specified identity. /// /// - /// Gets a substream of the specified message. If the starting offset is beyond - /// the end of the specified section of the message, an empty stream is returned. If - /// the number of bytes desired extends beyond the end of the section, a truncated - /// stream will be returned. - /// For more information about how to construct the , - /// see Section 6.4.5 of RFC3501. + /// Sets the access rights for the specified identity. /// - /// The stream. - /// The index of the message. - /// The desired section of the message. - /// The starting offset of the first desired byte. - /// The number of bytes desired. + /// An awaitable task. + /// The identity name. + /// The access rights. /// The cancellation token. - /// The progress reporting mechanism. /// - /// is null. - /// - /// - /// is out of range. + /// is . /// -or- - /// is negative. - /// -or- - /// is negative. + /// is . /// /// /// The has been disposed. @@ -7034,11 +3033,8 @@ static string GetBodyPartQuery (string partSpec, bool headersOnly, out string[] /// /// The is not authenticated. /// - /// - /// The is not currently open. - /// - /// - /// The IMAP server did not return the requested message stream. + /// + /// The IMAP server does not support the ACL extension. /// /// /// The operation was canceled via the cancellation token. @@ -7050,112 +3046,47 @@ static string GetBodyPartQuery (string partSpec, bool headersOnly, out string[] /// The server's response contained unexpected tokens. /// /// - /// The server replied with a NO or BAD response. + /// The command failed. /// - public override Stream GetStream (int index, string section, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + public override async Task SetAccessRightsAsync (string name, AccessRights rights, CancellationToken cancellationToken = default) { - if (index < 0 || index >= Count) - throw new ArgumentOutOfRangeException (nameof (index)); - - if (section == null) - throw new ArgumentNullException (nameof (section)); - - if (offset < 0) - throw new ArgumentOutOfRangeException (nameof (offset)); - - if (count < 0) - throw new ArgumentOutOfRangeException (nameof (count)); - - CheckState (true, false); - - if (count == 0) - return new MemoryStream (); - - var command = string.Format ("FETCH {0} (BODY.PEEK[{1}]<{2}.{3}>)\r\n", index + 1, section, offset, count); - var ic = new ImapCommand (Engine, cancellationToken, this, command); - var ctx = new FetchStreamContext (progress); - Stream stream; - - ic.RegisterUntaggedHandler ("FETCH", FetchStream); - ic.UserData = ctx; - - Engine.QueueCommand (ic); + var ic = QueueModifyAccessRightsCommand (name, string.Empty, rights, cancellationToken); - try { - Engine.Wait (ic); - - ProcessResponseCodes (ic, null); - - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("FETCH", ic); - - if (!ctx.Sections.TryGetValue (section, out stream)) - throw new MessageNotFoundException ("The IMAP server did not return the requested stream."); + await Engine.RunAsync (ic).ConfigureAwait (false); - ctx.Sections.Remove (section); - } finally { - ctx.Dispose (); - } - - return stream; + ProcessModifyAccessRightsResponse (ic); } - IList ModifyFlags (IList uids, ulong? modseq, MessageFlags flags, HashSet userFlags, string action, CancellationToken cancellationToken) + ImapCommand QueueRemoveAccessCommand (string name, CancellationToken cancellationToken) { - var flaglist = ImapUtils.FormatFlagsList (flags & PermanentFlags, userFlags != null ? userFlags.Count : 0); - var userFlagList = userFlags != null ? userFlags.ToArray () : new object[0]; - var set = ImapUtils.FormatUidSet (uids); - - if (modseq.HasValue && !SupportsModSeq) - throw new NotSupportedException ("The ImapFolder does not support mod-sequences."); - - CheckState (true, true); - - if (uids.Count == 0) - return new UniqueId[0]; + if (name == null) + throw new ArgumentNullException (nameof (name)); - string @params = string.Empty; - if (modseq.HasValue) - @params = string.Format (" (UNCHANGEDSINCE {0})", modseq.Value); + if ((Engine.Capabilities & ImapCapabilities.Acl) == 0) + throw new NotSupportedException ("The IMAP server does not support the ACL extension."); - var format = string.Format ("UID STORE {0}{1} {2} {3}\r\n", set, @params, action, flaglist); - var ic = Engine.QueueCommand (cancellationToken, this, format, userFlagList); + CheckState (false, false); - Engine.Wait (ic); + return Engine.QueueCommand (cancellationToken, null, "DELETEACL %F %S\r\n", this, name); + } + void ProcessRemoveAccessResponse (ImapCommand ic) + { ProcessResponseCodes (ic, null); - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("STORE", ic); - - if (modseq.HasValue) { - var modified = ic.RespCodes.OfType ().FirstOrDefault (); - - if (modified != null) - return modified.UidSet; - } - - return new UniqueId[0]; + ic.ThrowIfNotOk ("DELETEACL"); } /// - /// Adds a set of flags to the specified messages. + /// Remove all access rights for the given identity. /// /// - /// Adds a set of flags to the specified messages. + /// Removes all access rights for the given identity. /// - /// The UIDs of the messages. - /// The message flags to add. - /// A set of user-defined flags to add. - /// If set to true, no events will be emitted. + /// The identity name. /// The cancellation token. /// - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// No flags were specified. + /// is . /// /// /// The has been disposed. @@ -7166,8 +3097,8 @@ IList ModifyFlags (IList uids, ulong? modseq, MessageFlags f /// /// The is not authenticated. /// - /// - /// The is not currently open in read-write mode. + /// + /// The IMAP server does not support the ACL extension. /// /// /// The operation was canceled via the cancellation token. @@ -7179,36 +3110,28 @@ IList ModifyFlags (IList uids, ulong? modseq, MessageFlags f /// The server's response contained unexpected tokens. /// /// - /// The server replied with a NO or BAD response. + /// The command failed. /// - public override void AddFlags (IList uids, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + public override void RemoveAccess (string name, CancellationToken cancellationToken = default) { - bool emptyUserFlags = userFlags == null || userFlags.Count == 0; + var ic = QueueRemoveAccessCommand (name, cancellationToken); - if ((flags & SettableFlags) == 0 && emptyUserFlags) - throw new ArgumentException ("No flags were specified.", nameof (flags)); + Engine.Run (ic); - ModifyFlags (uids, null, flags, userFlags, silent ? "+FLAGS.SILENT" : "+FLAGS", cancellationToken); + ProcessRemoveAccessResponse (ic); } /// - /// Removes a set of flags from the specified messages. + /// Asynchronously remove all access rights for the given identity. /// /// - /// Removes a set of flags from the specified messages. + /// Removes all access rights for the given identity. /// - /// The UIDs of the messages. - /// The message flags to remove. - /// A set of user-defined flags to remove. - /// If set to true, no events will be emitted. + /// An awaitable task. + /// The identity name. /// The cancellation token. /// - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// No flags were specified. + /// is . /// /// /// The has been disposed. @@ -7219,8 +3142,8 @@ IList ModifyFlags (IList uids, ulong? modseq, MessageFlags f /// /// The is not authenticated. /// - /// - /// The is not currently open in read-write mode. + /// + /// The IMAP server does not support the ACL extension. /// /// /// The operation was canceled via the cancellation token. @@ -7232,33 +3155,66 @@ IList ModifyFlags (IList uids, ulong? modseq, MessageFlags f /// The server's response contained unexpected tokens. /// /// - /// The server replied with a NO or BAD response. + /// The command failed. /// - public override void RemoveFlags (IList uids, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + public override async Task RemoveAccessAsync (string name, CancellationToken cancellationToken = default) + { + var ic = QueueRemoveAccessCommand (name, cancellationToken); + + await Engine.RunAsync (ic).ConfigureAwait (false); + + ProcessRemoveAccessResponse (ic); + } + + ImapCommand QueueGetMetadataCommand (MetadataTag tag, CancellationToken cancellationToken) + { + CheckState (false, false); + + if ((Engine.Capabilities & ImapCapabilities.Metadata) == 0) + throw new NotSupportedException ("The IMAP server does not support the METADATA extension."); + + var ic = new ImapCommand (Engine, cancellationToken, null, "GETMETADATA %F %S\r\n", this, tag.Id); + ic.RegisterUntaggedHandler ("METADATA", ImapUtils.UntaggedMetadataHandler); + var metadata = new MetadataCollection (); + ic.UserData = metadata; + + Engine.QueueCommand (ic); + + return ic; + } + + string? ProcessGetMetadataResponse (ImapCommand ic, MetadataTag tag) { - if ((flags & SettableFlags) == 0 && (userFlags == null || userFlags.Count == 0)) - throw new ArgumentException ("No flags were specified.", nameof (flags)); + var metadata = (MetadataCollection) ic.UserData!; + + ProcessResponseCodes (ic, null); + + ic.ThrowIfNotOk ("GETMETADATA"); + + string? value = null; + + for (int i = 0; i < metadata.Count; i++) { + if (metadata[i].EncodedName == EncodedName && metadata[i].Tag.Id == tag.Id) { + value = metadata[i].Value; + metadata.RemoveAt (i); + break; + } + } - ModifyFlags (uids, null, flags, userFlags, silent ? "-FLAGS.SILENT" : "-FLAGS", cancellationToken); + Engine.ProcessMetadataChanges (metadata); + + return value; } /// - /// Sets the flags of the specified messages. + /// Get the specified metadata. /// /// - /// Sets the flags of the specified messages. + /// Gets the specified metadata. /// - /// The UIDs of the messages. - /// The message flags to set. - /// A set of user-defined flags to set. - /// If set to true, no events will be emitted. + /// The requested metadata value. + /// The metadata tag. /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the is invalid. - /// /// /// The has been disposed. /// @@ -7268,8 +3224,8 @@ IList ModifyFlags (IList uids, ulong? modseq, MessageFlags f /// /// The is not authenticated. /// - /// - /// The is not currently open in read-write mode. + /// + /// The IMAP server does not support the METADATA extension. /// /// /// The operation was canceled via the cancellation token. @@ -7283,32 +3239,24 @@ IList ModifyFlags (IList uids, ulong? modseq, MessageFlags f /// /// The server replied with a NO or BAD response. /// - public override void SetFlags (IList uids, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + public override string? GetMetadata (MetadataTag tag, CancellationToken cancellationToken = default) { - ModifyFlags (uids, null, flags, userFlags, silent ? "FLAGS.SILENT" : "FLAGS", cancellationToken); + var ic = QueueGetMetadataCommand (tag, cancellationToken); + + Engine.Run (ic); + + return ProcessGetMetadataResponse (ic, tag); } /// - /// Adds a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// Asynchronously get the specified metadata. /// /// - /// Adds a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// Gets the specified metadata. /// - /// The unique IDs of the messages that were not updated. - /// The UIDs of the messages. - /// The mod-sequence value. - /// The message flags to add. - /// A set of user-defined flags to add. - /// If set to true, no events will be emitted. + /// The requested metadata value. + /// The metadata tag. /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// No flags were specified. - /// /// /// The has been disposed. /// @@ -7318,11 +3266,8 @@ IList ModifyFlags (IList uids, ulong? modseq, MessageFlags f /// /// The is not authenticated. /// - /// - /// The is not currently open in read-write mode. - /// /// - /// The does not support mod-sequences. + /// The IMAP server does not support the METADATA extension. /// /// /// The operation was canceled via the cancellation token. @@ -7336,34 +3281,104 @@ IList ModifyFlags (IList uids, ulong? modseq, MessageFlags f /// /// The server replied with a NO or BAD response. /// - public override IList AddFlags (IList uids, ulong modseq, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + public override async Task GetMetadataAsync (MetadataTag tag, CancellationToken cancellationToken = default) + { + var ic = QueueGetMetadataCommand (tag, cancellationToken); + + await Engine.RunAsync (ic).ConfigureAwait (false); + + return ProcessGetMetadataResponse (ic, tag); + } + + ImapCommand? QueueGetMetadataCommand (MetadataOptions options, IEnumerable tags, CancellationToken cancellationToken) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (tags == null) + throw new ArgumentNullException (nameof (tags)); + + CheckState (false, false); + + if ((Engine.Capabilities & ImapCapabilities.Metadata) == 0) + throw new NotSupportedException ("The IMAP server does not support the METADATA extension."); + + var command = new StringBuilder ("GETMETADATA %F"); + var args = new List (); + bool hasOptions = false; + + if (options.MaxSize.HasValue || options.Depth != 0) { + command.Append (" ("); + if (options.MaxSize.HasValue) { + command.Append ("MAXSIZE "); + command.Append (options.MaxSize.Value.ToString (CultureInfo.InvariantCulture)); + command.Append (' '); + } + if (options.Depth > 0) { + command.Append ("DEPTH "); + command.Append (options.Depth == int.MaxValue ? "infinity" : "1"); + command.Append (' '); + } + command[command.Length - 1] = ')'; + command.Append (' '); + hasOptions = true; + } + + args.Add (this); + + int startIndex = command.Length; + foreach (var tag in tags) { + command.Append (" %S"); + args.Add (tag.Id); + } + + if (hasOptions) { + command[startIndex] = '('; + command.Append (')'); + } + + command.Append ("\r\n"); + + if (args.Count == 1) + return null; + + var ic = new ImapCommand (Engine, cancellationToken, null, command.ToString (), args.ToArray ()); + ic.RegisterUntaggedHandler ("METADATA", ImapUtils.UntaggedMetadataHandler); + ic.UserData = new MetadataCollection (); + options.LongEntries = 0; + + Engine.QueueCommand (ic); + + return ic; + } + + MetadataCollection ProcessGetMetadataResponse (ImapCommand ic, MetadataOptions options) { - if ((flags & SettableFlags) == 0 && (userFlags == null || userFlags.Count == 0)) - throw new ArgumentException ("No flags were specified.", nameof (flags)); + ProcessResponseCodes (ic, null); - return ModifyFlags (uids, modseq, flags, userFlags, silent ? "+FLAGS.SILENT" : "+FLAGS", cancellationToken); + ic.ThrowIfNotOk ("GETMETADATA"); + + var rc = ic.GetResponseCode (ImapResponseCodeType.Metadata); + if (rc is MetadataResponseCode metadata && metadata.SubType == MetadataResponseCodeSubType.LongEntries) + options.LongEntries = metadata.Value; + + return Engine.FilterMetadata ((MetadataCollection) ic.UserData!, EncodedName); } /// - /// Removes a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// Get the specified metadata. /// /// - /// Removes a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// Gets the specified metadata. /// - /// The unique IDs of the messages that were not updated. - /// The UIDs of the messages. - /// The mod-sequence value. - /// The message flags to remove. - /// A set of user-defined flags to remove. - /// If set to true, no events will be emitted. + /// The requested metadata. + /// The metadata options. + /// The metadata tags. /// The cancellation token. /// - /// is null. - /// - /// - /// One or more of the is invalid. + /// is . /// -or- - /// No flags were specified. + /// is . /// /// /// The has been disposed. @@ -7374,11 +3389,8 @@ IList ModifyFlags (IList uids, ulong? modseq, MessageFlags f /// /// The is not authenticated. /// - /// - /// The is not currently open in read-write mode. - /// /// - /// The does not support mod-sequences. + /// The IMAP server does not support the METADATA extension. /// /// /// The operation was canceled via the cancellation token. @@ -7392,32 +3404,32 @@ IList ModifyFlags (IList uids, ulong? modseq, MessageFlags f /// /// The server replied with a NO or BAD response. /// - public override IList RemoveFlags (IList uids, ulong modseq, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + public override MetadataCollection GetMetadata (MetadataOptions options, IEnumerable tags, CancellationToken cancellationToken = default) { - if ((flags & SettableFlags) == 0 && (userFlags == null || userFlags.Count == 0)) - throw new ArgumentException ("No flags were specified.", nameof (flags)); + var ic = QueueGetMetadataCommand (options, tags, cancellationToken); + + if (ic == null) + return new MetadataCollection (); + + Engine.Run (ic); - return ModifyFlags (uids, modseq, flags, userFlags, silent ? "-FLAGS.SILENT" : "-FLAGS", cancellationToken); + return ProcessGetMetadataResponse (ic, options); } /// - /// Sets the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// Asynchronously get the specified metadata. /// /// - /// Sets the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// Gets the specified metadata. /// - /// The unique IDs of the messages that were not updated. - /// The UIDs of the messages. - /// The mod-sequence value. - /// The message flags to set. - /// A set of user-defined flags to set. - /// If set to true, no events will be emitted. + /// The requested metadata. + /// The metadata options. + /// The metadata tags. /// The cancellation token. /// - /// is null. - /// - /// - /// One or more of the is invalid. + /// is . + /// -or- + /// is . /// /// /// The has been disposed. @@ -7428,11 +3440,8 @@ IList ModifyFlags (IList uids, ulong? modseq, MessageFlags f /// /// The is not authenticated. /// - /// - /// The is not currently open in read-write mode. - /// /// - /// The does not support mod-sequences. + /// The IMAP server does not support the METADATA extension. /// /// /// The operation was canceled via the cancellation token. @@ -7446,72 +3455,75 @@ IList ModifyFlags (IList uids, ulong? modseq, MessageFlags f /// /// The server replied with a NO or BAD response. /// - public override IList SetFlags (IList uids, ulong modseq, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + public override async Task GetMetadataAsync (MetadataOptions options, IEnumerable tags, CancellationToken cancellationToken = default) { - return ModifyFlags (uids, modseq, flags, userFlags, silent ? "FLAGS.SILENT" : "FLAGS", cancellationToken); + var ic = QueueGetMetadataCommand (options, tags, cancellationToken); + + if (ic == null) + return new MetadataCollection (); + + await Engine.RunAsync (ic).ConfigureAwait (false); + + return ProcessGetMetadataResponse (ic, options); } - IList ModifyFlags (IList indexes, ulong? modseq, MessageFlags flags, HashSet userFlags, string action, CancellationToken cancellationToken) + ImapCommand? QueueSetMetadataCommand (MetadataCollection metadata, CancellationToken cancellationToken) { - var flaglist = ImapUtils.FormatFlagsList (flags & PermanentFlags, userFlags != null ? userFlags.Count : 0); - var userFlagList = userFlags != null ? userFlags.ToArray () : new object[0]; - var set = ImapUtils.FormatIndexSet (indexes); - - if (modseq.HasValue && !SupportsModSeq) - throw new NotSupportedException ("The ImapFolder does not support mod-sequences."); + if (metadata == null) + throw new ArgumentNullException (nameof (metadata)); - CheckState (true, true); + CheckState (false, false); - if (indexes.Count == 0) - return new int[0]; + if ((Engine.Capabilities & ImapCapabilities.Metadata) == 0) + throw new NotSupportedException ("The IMAP server does not support the METADATA extension."); - string @params = string.Empty; - if (modseq.HasValue) - @params = string.Format (" (UNCHANGEDSINCE {0})", modseq.Value); + if (metadata.Count == 0) + return null; - var format = string.Format ("STORE {0}{1} {2} {3}\r\n", set, @params, action, flaglist); - var ic = Engine.QueueCommand (cancellationToken, this, format, userFlagList); + var command = new StringBuilder ("SETMETADATA %F ("); + var args = new List { + this + }; - Engine.Wait (ic); + for (int i = 0; i < metadata.Count; i++) { + if (i > 0) + command.Append (' '); - ProcessResponseCodes (ic, null); + if (metadata[i].Value != null) { + command.Append ("%S %S"); + args.Add (metadata[i].Tag.Id); + args.Add (metadata[i].Value); + } else { + command.Append ("%S NIL"); + args.Add (metadata[i].Tag.Id); + } + } + command.Append (")\r\n"); - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("STORE", ic); + var ic = new ImapCommand (Engine, cancellationToken, null, command.ToString (), args.ToArray ()); - if (modseq.HasValue) { - var modified = ic.RespCodes.OfType ().FirstOrDefault (); + Engine.QueueCommand (ic); - if (modified != null) { - var unmodified = new int[modified.UidSet.Count]; - for (int i = 0; i < unmodified.Length; i++) - unmodified[i] = (int) (modified.UidSet[i].Id - 1); + return ic; + } - return unmodified; - } - } + void ProcessSetMetadataResponse (ImapCommand ic) + { + ProcessResponseCodes (ic, null); - return new int[0]; + ic.ThrowIfNotOk ("SETMETADATA"); } /// - /// Adds a set of flags to the specified messages. + /// Set the specified metadata. /// /// - /// Adds a set of flags to the specified messages. + /// Sets the specified metadata. /// - /// The indexes of the messages. - /// The message flags to add. - /// A set of user-defined flags to add. - /// If set to true, no events will be emitted. + /// The metadata. /// The cancellation token. /// - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// No flags were specified. + /// is . /// /// /// The has been disposed. @@ -7522,8 +3534,8 @@ IList ModifyFlags (IList indexes, ulong? modseq, MessageFlags flags, H /// /// The is not authenticated. /// - /// - /// The is not currently open in read-write mode. + /// + /// The IMAP server does not support the METADATA extension. /// /// /// The operation was canceled via the cancellation token. @@ -7537,32 +3549,29 @@ IList ModifyFlags (IList indexes, ulong? modseq, MessageFlags flags, H /// /// The server replied with a NO or BAD response. /// - public override void AddFlags (IList indexes, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + public override void SetMetadata (MetadataCollection metadata, CancellationToken cancellationToken = default) { - if ((flags & SettableFlags) == 0 && (userFlags == null || userFlags.Count == 0)) - throw new ArgumentException ("No flags were specified.", nameof (flags)); + var ic = QueueSetMetadataCommand (metadata, cancellationToken); - ModifyFlags (indexes, null, flags, userFlags, silent ? "+FLAGS.SILENT" : "+FLAGS", cancellationToken); + if (ic == null) + return; + + Engine.Run (ic); + + ProcessSetMetadataResponse (ic); } /// - /// Removes a set of flags from the specified messages. + /// Asynchronously set the specified metadata. /// /// - /// Removes a set of flags from the specified messages. + /// Sets the specified metadata. /// - /// The indexes of the messages. - /// The message flags to remove. - /// A set of user-defined flags to remove. - /// If set to true, no events will be emitted. + /// An asynchronous task context. + /// The metadata. /// The cancellation token. /// - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// No flags were specified. + /// is . /// /// /// The has been disposed. @@ -7573,8 +3582,8 @@ IList ModifyFlags (IList indexes, ulong? modseq, MessageFlags flags, H /// /// The is not authenticated. /// - /// - /// The is not currently open in read-write mode. + /// + /// The IMAP server does not support the METADATA extension. /// /// /// The operation was canceled via the cancellation token. @@ -7588,137 +3597,267 @@ IList ModifyFlags (IList indexes, ulong? modseq, MessageFlags flags, H /// /// The server replied with a NO or BAD response. /// - public override void RemoveFlags (IList indexes, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + public override async Task SetMetadataAsync (MetadataCollection metadata, CancellationToken cancellationToken = default) + { + var ic = QueueSetMetadataCommand (metadata, cancellationToken); + + if (ic == null) + return; + + await Engine.RunAsync (ic).ConfigureAwait (false); + + ProcessSetMetadataResponse (ic); + } + + class Quota + { + public uint? MessageLimit; + public uint? StorageLimit; + public uint? CurrentMessageCount; + public uint? CurrentStorageSize; + } + + class QuotaContext + { + public QuotaContext () + { + Quotas = new Dictionary (); + QuotaRoots = new List (); + } + + public List QuotaRoots { + get; private set; + } + + public Dictionary Quotas { + get; private set; + } + } + + static void ParseQuotaRoot (ImapEngine engine, ImapCommand ic) + { + var format = string.Format (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "QUOTAROOT", "{0}"); + var ctx = (QuotaContext) ic.UserData!; + + // The first token should be the mailbox name + ImapUtils.ReadFolderName (engine, format, false, ic.CancellationToken); + + // ...followed by 0 or more quota roots + var token = engine.PeekToken (ImapStream.AtomSpecials, ic.CancellationToken); + + while (token.Type != ImapTokenType.Eoln) { + var root = ImapUtils.ReadFolderName (engine, format, false, ic.CancellationToken); + ctx.QuotaRoots.Add (root); + + token = engine.PeekToken (ImapStream.AtomSpecials, ic.CancellationToken); + } + } + + static async Task ParseQuotaRootAsync (ImapEngine engine, ImapCommand ic) + { + var format = string.Format (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "QUOTAROOT", "{0}"); + var ctx = (QuotaContext) ic.UserData!; + + // The first token should be the mailbox name + await ImapUtils.ReadFolderNameAsync (engine, format, false, ic.CancellationToken).ConfigureAwait (false); + + // ...followed by 0 or more quota roots + var token = await engine.PeekTokenAsync (ImapStream.AtomSpecials, ic.CancellationToken).ConfigureAwait (false); + + while (token.Type != ImapTokenType.Eoln) { + var root = await ImapUtils.ReadFolderNameAsync (engine, format, false, ic.CancellationToken).ConfigureAwait (false); + ctx.QuotaRoots.Add (root); + + token = await engine.PeekTokenAsync (ImapStream.AtomSpecials, ic.CancellationToken).ConfigureAwait (false); + } + } + + /// + /// Handles an untagged QUOTAROOT response. + /// + /// An asynchronous task. + /// The IMAP engine. + /// The IMAP command. + /// The index. + /// Whether or not asynchronous IO methods should be used. + static Task UntaggedQuotaRootHandler (ImapEngine engine, ImapCommand ic, int index, bool doAsync) + { + if (doAsync) + return ParseQuotaRootAsync (engine, ic); + + ParseQuotaRoot (engine, ic); + + return Task.CompletedTask; + } + + static void ParseQuota (ImapEngine engine, ImapCommand ic) + { + var format = string.Format (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "QUOTA", "{0}"); + var quotaRoot = ImapUtils.ReadFolderName (engine, format, false, ic.CancellationToken); + var ctx = (QuotaContext) ic.UserData!; + var quota = new Quota (); + + var token = engine.ReadToken (ic.CancellationToken); + + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, format, token); + + while (token.Type != ImapTokenType.CloseParen) { + ulong used, limit; + string resource; + + token = engine.ReadToken (ic.CancellationToken); + + ImapEngine.AssertToken (token, ImapTokenType.Atom, format, token); + + resource = (string) token.Value; + + token = engine.ReadToken (ic.CancellationToken); + + // Note: We parse these quota values as UInt64 because GMail uses 64bit integer values. + // See https://github.com/jstedfast/MailKit/issues/1602 for details. + used = ImapEngine.ParseNumber64 (token, false, format, token); + + token = engine.ReadToken (ic.CancellationToken); + + // Note: We parse these quota values as UInt64 because GMail uses 64bit integer values. + // See https://github.com/jstedfast/MailKit/issues/1602 for details. + limit = ImapEngine.ParseNumber64 (token, false, format, token); + + if (resource.Equals ("MESSAGE", StringComparison.OrdinalIgnoreCase)) { + quota.CurrentMessageCount = (uint) (used & 0xffffffff); + quota.MessageLimit = (uint) (limit & 0xffffffff); + } else if (resource.Equals ("STORAGE", StringComparison.OrdinalIgnoreCase)) { + quota.CurrentStorageSize = (uint) (used & 0xffffffff); + quota.StorageLimit = (uint) (limit & 0xffffffff); + } + + token = engine.PeekToken (ic.CancellationToken); + } + + // read the closing paren + engine.ReadToken (ic.CancellationToken); + + ctx.Quotas[quotaRoot] = quota; + } + + static async Task ParseQuotaAsync (ImapEngine engine, ImapCommand ic) { - if ((flags & SettableFlags) == 0 && (userFlags == null || userFlags.Count == 0)) - throw new ArgumentException ("No flags were specified.", nameof (flags)); + var format = string.Format (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "QUOTA", "{0}"); + var quotaRoot = await ImapUtils.ReadFolderNameAsync (engine, format, false, ic.CancellationToken).ConfigureAwait (false); + var ctx = (QuotaContext) ic.UserData!; + var quota = new Quota (); + + var token = await engine.ReadTokenAsync (ic.CancellationToken).ConfigureAwait (false); + + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, format, token); + + while (token.Type != ImapTokenType.CloseParen) { + ulong used, limit; + string resource; + + token = await engine.ReadTokenAsync (ic.CancellationToken).ConfigureAwait (false); + + ImapEngine.AssertToken (token, ImapTokenType.Atom, format, token); + + resource = (string) token.Value; + + token = await engine.ReadTokenAsync (ic.CancellationToken).ConfigureAwait (false); - ModifyFlags (indexes, null, flags, userFlags, silent ? "-FLAGS.SILENT" : "-FLAGS", cancellationToken); + // Note: We parse these quota values as UInt64 because GMail uses 64bit integer values. + // See https://github.com/jstedfast/MailKit/issues/1602 for details. + used = ImapEngine.ParseNumber64 (token, false, format, token); + + token = await engine.ReadTokenAsync (ic.CancellationToken).ConfigureAwait (false); + + // Note: We parse these quota values as UInt64 because GMail uses 64bit integer values. + // See https://github.com/jstedfast/MailKit/issues/1602 for details. + limit = ImapEngine.ParseNumber64 (token, false, format, token); + + if (resource.Equals ("MESSAGE", StringComparison.OrdinalIgnoreCase)) { + quota.CurrentMessageCount = (uint) (used & 0xffffffff); + quota.MessageLimit = (uint) (limit & 0xffffffff); + } else if (resource.Equals ("STORAGE", StringComparison.OrdinalIgnoreCase)) { + quota.CurrentStorageSize = (uint) (used & 0xffffffff); + quota.StorageLimit = (uint) (limit & 0xffffffff); + } + + token = await engine.PeekTokenAsync (ic.CancellationToken).ConfigureAwait (false); + } + + // read the closing paren + await engine.ReadTokenAsync (ic.CancellationToken).ConfigureAwait (false); + + ctx.Quotas[quotaRoot] = quota; } /// - /// Sets the flags of the specified messages. + /// Handles an untagged QUOTA response. /// - /// - /// Sets the flags of the specified messages. - /// - /// The indexes of the messages. - /// The message flags to set. - /// A set of user-defined flags to set. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the is invalid. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The is not currently open in read-write mode. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The server replied with a NO or BAD response. - /// - public override void SetFlags (IList indexes, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + /// An asynchronous task. + /// The IMAP engine. + /// The IMAP command. + /// The index. + /// Whether or not asynchronous IO methods should be used. + static Task UntaggedQuotaHandler (ImapEngine engine, ImapCommand ic, int index, bool doAsync) { - ModifyFlags (indexes, null, flags, userFlags, silent ? "FLAGS.SILENT" : "FLAGS", cancellationToken); + if (doAsync) + return ParseQuotaAsync (engine, ic); + + ParseQuota (engine, ic); + + return Task.CompletedTask; } - /// - /// Adds a set of flags to the specified messages only if their mod-sequence value is less than the specified value. - /// - /// - /// Adds a set of flags to the specified messages only if their mod-sequence value is less than the specified value. - /// - /// The indexes of the messages that were not updated. - /// The indexes of the messages. - /// The mod-sequence value. - /// The message flags to add. - /// A set of user-defined flags to add. - /// If set to true, no events will be emitted. - /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// No flags were specified. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The is not currently open in read-write mode. - /// - /// - /// The does not support mod-sequences. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The server's response contained unexpected tokens. - /// - /// - /// The server replied with a NO or BAD response. - /// - public override IList AddFlags (IList indexes, ulong modseq, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + ImapCommand QueueGetQuotaCommand (CancellationToken cancellationToken) { - if ((flags & SettableFlags) == 0 && (userFlags == null || userFlags.Count == 0)) - throw new ArgumentException ("No flags were specified.", nameof (flags)); + CheckState (false, false); + + if ((Engine.Capabilities & ImapCapabilities.Quota) == 0) + throw new NotSupportedException ("The IMAP server does not support the QUOTA extension."); + + var ic = new ImapCommand (Engine, cancellationToken, null, "GETQUOTAROOT %F\r\n", this); + var ctx = new QuotaContext (); + + ic.RegisterUntaggedHandler ("QUOTAROOT", UntaggedQuotaRootHandler); + ic.RegisterUntaggedHandler ("QUOTA", UntaggedQuotaHandler); + ic.UserData = ctx; + + Engine.QueueCommand (ic); + + return ic; + } + + bool TryProcessGetQuotaResponse (ImapCommand ic, [NotNullWhen (true)] out string? encodedName, [NotNullWhen (true)] out Quota? quota) + { + var ctx = (QuotaContext) ic.UserData!; - return ModifyFlags (indexes, modseq, flags, userFlags, silent ? "+FLAGS.SILENT" : "+FLAGS", cancellationToken); + ProcessResponseCodes (ic, null); + + ic.ThrowIfNotOk ("GETQUOTAROOT"); + + for (int i = 0; i < ctx.QuotaRoots.Count; i++) { + encodedName = ctx.QuotaRoots[i]; + + if (ctx.Quotas.TryGetValue (encodedName, out quota)) + return true; + } + + encodedName = null; + quota = null; + + return false; } /// - /// Removes a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// Get the quota information for the folder. /// /// - /// Removes a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// Gets the quota information for the folder. + /// To determine if a quotas are supported, check the + /// property. /// - /// The indexes of the messages that were not updated. - /// The indexes of the messages. - /// The mod-sequence value. - /// The message flags to remove. - /// A set of user-defined flags to remove. - /// If set to true, no events will be emitted. + /// The folder quota. /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// No flags were specified. - /// /// /// The has been disposed. /// @@ -7728,11 +3867,8 @@ IList ModifyFlags (IList indexes, ulong? modseq, MessageFlags flags, H /// /// The is not authenticated. /// - /// - /// The is not currently open in read-write mode. - /// /// - /// The does not support mod-sequences. + /// The IMAP server does not support the QUOTA extension. /// /// /// The operation was canceled via the cancellation token. @@ -7746,33 +3882,35 @@ IList ModifyFlags (IList indexes, ulong? modseq, MessageFlags flags, H /// /// The server replied with a NO or BAD response. /// - public override IList RemoveFlags (IList indexes, ulong modseq, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + public override FolderQuota GetQuota (CancellationToken cancellationToken = default) { - if ((flags & SettableFlags) == 0 && (userFlags == null || userFlags.Count == 0)) - throw new ArgumentException ("No flags were specified.", nameof (flags)); + var ic = QueueGetQuotaCommand (cancellationToken); + + Engine.Run (ic); - return ModifyFlags (indexes, modseq, flags, userFlags, silent ? "-FLAGS.SILENT" : "-FLAGS", cancellationToken); + if (!TryProcessGetQuotaResponse (ic, out var encodedName, out var quota)) + return new FolderQuota (null); + + var quotaRoot = Engine.GetQuotaRootFolder (encodedName, cancellationToken); + + return new FolderQuota (quotaRoot) { + CurrentMessageCount = quota.CurrentMessageCount, + CurrentStorageSize = quota.CurrentStorageSize, + MessageLimit = quota.MessageLimit, + StorageLimit = quota.StorageLimit + }; } /// - /// Sets the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// Asynchronously get the quota information for the folder. /// /// - /// Sets the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// Gets the quota information for the folder. + /// To determine if a quotas are supported, check the + /// property. /// - /// The indexes of the messages that were not updated. - /// The indexes of the messages. - /// The mod-sequence value. - /// The message flags to set. - /// A set of user-defined flags to set. - /// If set to true, no events will be emitted. + /// The folder quota. /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the is invalid. - /// /// /// The has been disposed. /// @@ -7782,11 +3920,8 @@ IList ModifyFlags (IList indexes, ulong? modseq, MessageFlags flags, H /// /// The is not authenticated. /// - /// - /// The is not currently open in read-write mode. - /// /// - /// The does not support mod-sequences. + /// The IMAP server does not support the QUOTA extension. /// /// /// The operation was canceled via the cancellation token. @@ -7800,105 +3935,89 @@ IList ModifyFlags (IList indexes, ulong? modseq, MessageFlags flags, H /// /// The server replied with a NO or BAD response. /// - public override IList SetFlags (IList indexes, ulong modseq, MessageFlags flags, HashSet userFlags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) - { - return ModifyFlags (indexes, modseq, flags, userFlags, silent ? "FLAGS.SILENT" : "FLAGS", cancellationToken); - } - - static string LabelListToString (IList labels, ICollection args) + public override async Task GetQuotaAsync (CancellationToken cancellationToken = default) { - var list = new StringBuilder ("("); - - for (int i = 0; i < labels.Count; i++) { - if (i > 0) - list.Append (' '); + var ic = QueueGetQuotaCommand (cancellationToken); - if (labels[i] == null) { - list.Append ("NIL"); - continue; - } + await Engine.RunAsync (ic).ConfigureAwait (false); - switch (labels[i]) { - case "\\AllMail": - case "\\Drafts": - case "\\Important": - case "\\Inbox": - case "\\Spam": - case "\\Sent": - case "\\Starred": - case "\\Trash": - list.Append (labels[i]); - break; - default: - list.Append ("%S"); - args.Add (ImapEncoding.Encode (labels[i])); - break; - } - } + if (!TryProcessGetQuotaResponse (ic, out var encodedName, out var quota)) + return new FolderQuota (null); - list.Append (')'); + var quotaRoot = await Engine.GetQuotaRootFolderAsync (encodedName, cancellationToken).ConfigureAwait (false); - return list.ToString (); + return new FolderQuota (quotaRoot) { + CurrentMessageCount = quota.CurrentMessageCount, + CurrentStorageSize = quota.CurrentStorageSize, + MessageLimit = quota.MessageLimit, + StorageLimit = quota.StorageLimit + }; } - IList ModifyLabels (IList uids, ulong? modseq, IList labels, string action, CancellationToken cancellationToken) + ImapCommand QueueSetQuotaCommand (uint? messageLimit, uint? storageLimit, CancellationToken cancellationToken) { - var set = ImapUtils.FormatUidSet (uids); + CheckState (false, false); - if ((Engine.Capabilities & ImapCapabilities.GMailExt1) == 0) - throw new NotSupportedException ("The IMAP server does not support the Google Mail extensions."); + if ((Engine.Capabilities & ImapCapabilities.Quota) == 0) + throw new NotSupportedException ("The IMAP server does not support the QUOTA extension."); - CheckState (true, true); + var command = new StringBuilder ("SETQUOTA %F ("); + if (messageLimit.HasValue) { + command.Append ("MESSAGE "); + command.Append (messageLimit.Value.ToString (CultureInfo.InvariantCulture)); + command.Append (' '); + } + if (storageLimit.HasValue) { + command.Append ("STORAGE "); + command.Append (storageLimit.Value.ToString (CultureInfo.InvariantCulture)); + command.Append (' '); + } + command[command.Length - 1] = ')'; + command.Append ("\r\n"); - if (uids.Count == 0) - return new UniqueId[0]; + var ic = new ImapCommand (Engine, cancellationToken, null, command.ToString (), this); + var ctx = new QuotaContext (); - string @params = string.Empty; - if (modseq.HasValue) - @params = string.Format (" (UNCHANGEDSINCE {0})", modseq.Value); + ic.RegisterUntaggedHandler ("QUOTA", UntaggedQuotaHandler); + ic.UserData = ctx; - var args = new List (); - var list = LabelListToString (labels, args); - var format = string.Format ("UID STORE {0}{1} {2} {3}\r\n", set, @params, action, list); - var ic = Engine.QueueCommand (cancellationToken, this, format, args.ToArray ()); + Engine.QueueCommand (ic); - Engine.Wait (ic); + return ic; + } - ProcessResponseCodes (ic, null); + FolderQuota ProcessSetQuotaResponse (ImapCommand ic) + { + var ctx = (QuotaContext) ic.UserData!; - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("STORE", ic); + ProcessResponseCodes (ic, null); - if (modseq.HasValue) { - var modified = ic.RespCodes.OfType ().FirstOrDefault (); + ic.ThrowIfNotOk ("SETQUOTA"); - if (modified != null) - return modified.UidSet; + if (ctx.Quotas.TryGetValue (EncodedName, out var quota)) { + return new FolderQuota (this) { + CurrentMessageCount = quota.CurrentMessageCount, + CurrentStorageSize = quota.CurrentStorageSize, + MessageLimit = quota.MessageLimit, + StorageLimit = quota.StorageLimit + }; } - return new UniqueId[0]; + return new FolderQuota (null); } /// - /// Add a set of labels to the specified messages. + /// Set the quota limits for the folder. /// /// - /// Adds a set of labels to the specified messages. + /// Sets the quota limits for the folder. + /// To determine if a quotas are supported, check the + /// property. /// - /// The UIDs of the messages. - /// The labels to add. - /// If set to true, no events will be emitted. + /// The folder quota. + /// If not , sets the maximum number of messages to allow. + /// If not , sets the maximum storage size (in kilobytes). /// The cancellation token. - /// - /// is null. - /// -or- - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// No labels were specified. - /// /// /// The has been disposed. /// @@ -7908,8 +4027,8 @@ IList ModifyLabels (IList uids, ulong? modseq, IList /// /// The is not authenticated. /// - /// - /// The is not currently open in read-write mode. + /// + /// The IMAP server does not support the QUOTA extension. /// /// /// The operation was canceled via the cancellation token. @@ -7923,37 +4042,27 @@ IList ModifyLabels (IList uids, ulong? modseq, IList /// /// The server replied with a NO or BAD response. /// - public override void AddLabels (IList uids, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + public override FolderQuota SetQuota (uint? messageLimit, uint? storageLimit, CancellationToken cancellationToken = default) { - if (labels == null) - throw new ArgumentNullException (nameof (labels)); + var ic = QueueSetQuotaCommand (messageLimit, storageLimit, cancellationToken); - if (labels.Count == 0) - throw new ArgumentException ("No labels were specified.", nameof (labels)); + Engine.Run (ic); - ModifyLabels (uids, null, labels, silent ? "+X-GM-LABELS.SILENT" : "+X-GM-LABELS", cancellationToken); + return ProcessSetQuotaResponse (ic); } /// - /// Remove a set of labels from the specified messages. + /// Asynchronously set the quota limits for the folder. /// /// - /// Removes a set of labels from the specified messages. + /// Sets the quota limits for the folder. + /// To determine if a quotas are supported, check the + /// property. /// - /// The UIDs of the messages. - /// The labels to remove. - /// If set to true, no events will be emitted. + /// The folder quota. + /// If not , sets the maximum number of messages to allow. + /// If not , sets the maximum storage size (in kilobytes). /// The cancellation token. - /// - /// is null. - /// -or- - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// No labels were specified. - /// /// /// The has been disposed. /// @@ -7963,8 +4072,8 @@ IList ModifyLabels (IList uids, ulong? modseq, IList /// /// The is not authenticated. /// - /// - /// The is not currently open in read-write mode. + /// + /// The IMAP server does not support the QUOTA extension. /// /// /// The operation was canceled via the cancellation token. @@ -7978,46 +4087,55 @@ IList ModifyLabels (IList uids, ulong? modseq, IList /// /// The server replied with a NO or BAD response. /// - public override void RemoveLabels (IList uids, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + public override async Task SetQuotaAsync (uint? messageLimit, uint? storageLimit, CancellationToken cancellationToken = default) + { + var ic = QueueSetQuotaCommand (messageLimit, storageLimit, cancellationToken); + + await Engine.RunAsync (ic).ConfigureAwait (false); + + return ProcessSetQuotaResponse (ic); + } + + ImapCommand QueueExpungeCommand (CancellationToken cancellationToken) { - if (labels == null) - throw new ArgumentNullException (nameof (labels)); + CheckState (true, true); + + return Engine.QueueCommand (cancellationToken, this, "EXPUNGE\r\n"); + } - if (labels.Count == 0) - throw new ArgumentException ("No labels were specified.", nameof (labels)); + void ProcessExpungeResponse (ImapCommand ic) + { + ProcessResponseCodes (ic, null); - ModifyLabels (uids, null, labels, silent ? "-X-GM-LABELS.SILENT" : "-X-GM-LABELS", cancellationToken); + ic.ThrowIfNotOk ("EXPUNGE"); } /// - /// Set the labels of the specified messages. + /// Expunge the folder, permanently removing all messages marked for deletion. /// /// - /// Sets the labels of the specified messages. + /// The EXPUNGE command permanently removes all messages in the folder + /// that have the flag set. + /// For more information about the EXPUNGE command, see + /// rfc3501. + /// Normally, a event will be emitted + /// for each message that is expunged. However, if the IMAP server supports the QRESYNC extension + /// and it has been enabled via the + /// method, then the event will be emitted rather than + /// the event. /// - /// The UIDs of the messages. - /// The labels to set. - /// If set to true, no events will be emitted. /// The cancellation token. - /// - /// is null. - /// -or- - /// is null. - /// - /// - /// One or more of the is invalid. - /// /// /// The has been disposed. /// + /// + /// The is not currently open in read-write mode. + /// /// /// The is not connected. /// - /// - /// The is not authenticated. - /// - /// - /// The is not currently open in read-write mode. + /// + /// The is not authenticated. /// /// /// The operation was canceled via the cancellation token. @@ -8031,48 +4149,43 @@ IList ModifyLabels (IList uids, ulong? modseq, IList /// /// The server replied with a NO or BAD response. /// - public override void SetLabels (IList uids, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + public override void Expunge (CancellationToken cancellationToken = default) { - if (labels == null) - throw new ArgumentNullException (nameof (labels)); + var ic = QueueExpungeCommand (cancellationToken); - ModifyLabels (uids, null, labels, silent ? "X-GM-LABELS.SILENT" : "X-GM-LABELS", cancellationToken); + Engine.Run (ic); + + ProcessExpungeResponse (ic); } /// - /// Add a set of labels to the specified messages only if their mod-sequence value is less than the specified value. + /// Asynchronously expunge the folder, permanently removing all messages marked for deletion. /// /// - /// Adds a set of labels to the specified messages only if their mod-sequence value is less than the specified value. + /// The EXPUNGE command permanently removes all messages in the folder + /// that have the flag set. + /// For more information about the EXPUNGE command, see + /// rfc3501. + /// Normally, a event will be emitted + /// for each message that is expunged. However, if the IMAP server supports the QRESYNC extension + /// and it has been enabled via the + /// method, then the event will be emitted rather than + /// the event. /// - /// The unique IDs of the messages that were not updated. - /// The UIDs of the messages. - /// The mod-sequence value. - /// The labels to add. - /// If set to true, no events will be emitted. + /// An asynchronous task context. /// The cancellation token. - /// - /// is null. - /// -or- - /// is null. - /// - /// - /// One or more of the is invalid. - /// -or- - /// No labels were specified. - /// /// /// The has been disposed. /// + /// + /// The is not currently open in read-write mode. + /// /// /// The is not connected. /// /// /// The is not authenticated. /// - /// - /// The is not currently open in read-write mode. - /// /// /// The operation was canceled via the cancellation token. /// @@ -8085,38 +4198,45 @@ IList ModifyLabels (IList uids, ulong? modseq, IList /// /// The server replied with a NO or BAD response. /// - public override IList AddLabels (IList uids, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + public override async Task ExpungeAsync (CancellationToken cancellationToken = default) { - if (labels == null) - throw new ArgumentNullException (nameof (labels)); + var ic = QueueExpungeCommand (cancellationToken); - if (labels.Count == 0) - throw new ArgumentException ("No labels were specified.", nameof (labels)); + await Engine.RunAsync (ic).ConfigureAwait (false); - return ModifyLabels (uids, modseq, labels, silent ? "+X-GM-LABELS.SILENT" : "+X-GM-LABELS", cancellationToken); + ProcessExpungeResponse (ic); } /// - /// Remove a set of labels from the specified messages only if their mod-sequence value is less than the specified value. + /// Expunge the specified uids, permanently removing them from the folder. /// /// - /// Removes a set of labels from the specified messages only if their mod-sequence value is less than the specified value. + /// Expunges the specified uids, permanently removing them from the folder. + /// If the IMAP server supports the UIDPLUS extension (check the + /// for the + /// flag), then this operation is atomic. Otherwise, MailKit implements this operation + /// by first searching for the full list of message uids in the folder that are marked for + /// deletion, unmarking the set of message uids that are not within the specified list of + /// uids to be be expunged, expunging the folder (thus expunging the requested uids), and + /// finally restoring the deleted flag on the collection of message uids that were originally + /// marked for deletion that were not included in the list of uids provided. For this reason, + /// it is advisable for clients that wish to maintain state to implement this themselves when + /// the IMAP server does not support the UIDPLUS extension. + /// For more information about the UID EXPUNGE command, see + /// rfc4315. + /// Normally, a event will be emitted + /// for each message that is expunged. However, if the IMAP server supports the QRESYNC extension + /// and it has been enabled via the + /// method, then the event will be emitted rather than + /// the event. /// - /// The unique IDs of the messages that were not updated. - /// The UIDs of the messages. - /// The mod-sequence value. - /// The labels to remove. - /// If set to true, no events will be emitted. + /// The message uids. /// The cancellation token. /// - /// is null. - /// -or- - /// is null. + /// is . /// /// - /// One or more of the is invalid. - /// -or- - /// No labels were specified. + /// One or more of the is invalid. /// /// /// The has been disposed. @@ -8142,33 +4262,72 @@ IList ModifyLabels (IList uids, ulong? modseq, IList /// /// The server replied with a NO or BAD response. /// - public override IList RemoveLabels (IList uids, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + public override void Expunge (IList uids, CancellationToken cancellationToken = default) { - if (labels == null) - throw new ArgumentNullException (nameof (labels)); + if (uids == null) + throw new ArgumentNullException (nameof (uids)); + + CheckState (true, true); + + if (uids.Count == 0) + return; + + if ((Engine.Capabilities & ImapCapabilities.UidPlus) == 0) { + // get the list of messages marked for deletion that should not be expunged + var query = SearchQuery.Deleted.And (SearchQuery.Not (SearchQuery.Uids (uids))); + var unmark = Search (SearchOptions.None, query, cancellationToken); + + if (unmark.Count > 0) { + // clear the \Deleted flag on all messages except the ones that are to be expunged + Store (unmark.UniqueIds, RemoveDeletedFlag, cancellationToken); + } + + // expunge the folder + Expunge (cancellationToken); + + if (unmark.Count > 0) { + // restore the \Deleted flags + Store (unmark.UniqueIds, AddDeletedFlag, cancellationToken); + } + + return; + } - if (labels.Count == 0) - throw new ArgumentException ("No labels were specified.", nameof (labels)); + foreach (var ic in Engine.QueueCommands (cancellationToken, this, "UID EXPUNGE %s\r\n", uids)) { + Engine.Run (ic); - return ModifyLabels (uids, modseq, labels, silent ? "-X-GM-LABELS.SILENT" : "-X-GM-LABELS", cancellationToken); + ProcessExpungeResponse (ic); + } } /// - /// Set the labels of the specified messages only if their mod-sequence value is less than the specified value. + /// Asynchronously expunge the specified uids, permanently removing them from the folder. /// /// - /// Sets the labels of the specified messages only if their mod-sequence value is less than the specified value. + /// Expunges the specified uids, permanently removing them from the folder. + /// If the IMAP server supports the UIDPLUS extension (check the + /// for the + /// flag), then this operation is atomic. Otherwise, MailKit implements this operation + /// by first searching for the full list of message uids in the folder that are marked for + /// deletion, unmarking the set of message uids that are not within the specified list of + /// uids to be be expunged, expunging the folder (thus expunging the requested uids), and + /// finally restoring the deleted flag on the collection of message uids that were originally + /// marked for deletion that were not included in the list of uids provided. For this reason, + /// it is advisable for clients that wish to maintain state to implement this themselves when + /// the IMAP server does not support the UIDPLUS extension. + /// For more information about the UID EXPUNGE command, see + /// rfc4315. + /// Normally, a event will be emitted + /// for each message that is expunged. However, if the IMAP server supports the QRESYNC extension + /// and it has been enabled via the + /// method, then the event will be emitted rather than + /// the event. /// - /// The unique IDs of the messages that were not updated. - /// The UIDs of the messages. - /// The mod-sequence value. - /// The labels to set. - /// If set to true, no events will be emitted. + /// An asynchronous task context. + /// The message uids. /// The cancellation token. /// - /// is null. - /// -or- - /// is null. + /// is . /// /// /// One or more of the is invalid. @@ -8197,76 +4356,146 @@ IList ModifyLabels (IList uids, ulong? modseq, IList /// /// The server replied with a NO or BAD response. /// - public override IList SetLabels (IList uids, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + public override async Task ExpungeAsync (IList uids, CancellationToken cancellationToken = default) { - if (labels == null) - throw new ArgumentNullException (nameof (labels)); + if (uids == null) + throw new ArgumentNullException (nameof (uids)); - return ModifyLabels (uids, modseq, labels, silent ? "X-GM-LABELS.SILENT" : "X-GM-LABELS", cancellationToken); + CheckState (true, true); + + if (uids.Count == 0) + return; + + if ((Engine.Capabilities & ImapCapabilities.UidPlus) == 0) { + // get the list of messages marked for deletion that should not be expunged + var query = SearchQuery.Deleted.And (SearchQuery.Not (SearchQuery.Uids (uids))); + var unmark = await SearchAsync (SearchOptions.None, query, cancellationToken).ConfigureAwait (false); + + if (unmark.Count > 0) { + // clear the \Deleted flag on all messages except the ones that are to be expunged + await StoreAsync (unmark.UniqueIds, RemoveDeletedFlag, cancellationToken).ConfigureAwait (false); + } + + // expunge the folder + await ExpungeAsync (cancellationToken).ConfigureAwait (false); + + if (unmark.Count > 0) { + // restore the \Deleted flags + await StoreAsync (unmark.UniqueIds, AddDeletedFlag, cancellationToken).ConfigureAwait (false); + } + + return; + } + + foreach (var ic in Engine.QueueCommands (cancellationToken, this, "UID EXPUNGE %s\r\n", uids)) { + await Engine.RunAsync (ic).ConfigureAwait (false); + + ProcessExpungeResponse (ic); + } } - IList ModifyLabels (IList indexes, ulong? modseq, IList labels, string action, CancellationToken cancellationToken) + FormatOptions CreateAppendOptions (FormatOptions options) { - var set = ImapUtils.FormatIndexSet (indexes); + if (options.International && (Engine.Capabilities & ImapCapabilities.UTF8Accept) == 0) + throw new NotSupportedException ("The IMAP server does not support the UTF8 extension."); - if ((Engine.Capabilities & ImapCapabilities.GMailExt1) == 0) - throw new NotSupportedException ("The IMAP server does not support the Google Mail extensions."); + var format = options.Clone (); + format.NewLineFormat = NewLineFormat.Dos; + format.EnsureNewLine = true; - CheckState (true, true); + if ((Engine.Capabilities & ImapCapabilities.UTF8Only) == ImapCapabilities.UTF8Only) + format.International = true; - if (indexes.Count == 0) - return new int[0]; + if (format.International && !Engine.UTF8Enabled) + throw new InvalidOperationException ("The UTF8 extension has not been enabled."); - string @params = string.Empty; - if (modseq.HasValue) - @params = string.Format (" (UNCHANGEDSINCE {0})", modseq.Value); + return format; + } - var args = new List (); - var list = LabelListToString (labels, args); - var format = string.Format ("STORE {0}{1} {2} {3}\r\n", set, @params, action, list); - var ic = Engine.QueueCommand (cancellationToken, this, format, args.ToArray ()); + ImapCommand QueueAppendCommand (FormatOptions options, IAppendRequest request, CancellationToken cancellationToken) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); - Engine.Wait (ic); + if (request == null) + throw new ArgumentNullException (nameof (request)); - ProcessResponseCodes (ic, null); + CheckState (false, false); - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("STORE", ic); + var format = CreateAppendOptions (options); - if (modseq.HasValue) { - var modified = ic.RespCodes.OfType ().FirstOrDefault (); + if (request.Annotations != null && request.Annotations.Count > 0 && (Engine.Capabilities & ImapCapabilities.Annotate) == 0) + throw new NotSupportedException ("The IMAP server does not support annotations."); - if (modified != null) { - var unmodified = new int[modified.UidSet.Count]; - for (int i = 0; i < unmodified.Length; i++) - unmodified[i] = (int) (modified.UidSet[i].Id - 1); + int numKeywords = request.Keywords != null ? request.Keywords.Count : 0; + var builder = new StringBuilder ("APPEND %F "); + var list = new List { + this + }; - return unmodified; - } + if ((request.Flags & SettableFlags) != 0 || numKeywords > 0) { + ImapUtils.FormatFlagsList (builder, request.Flags, numKeywords); + builder.Append (' '); + } + + if (request.Keywords != null) { + foreach (var keyword in request.Keywords) + list.Add (keyword); + } + + if (request.InternalDate.HasValue) { + builder.Append ('"'); + builder.Append (ImapUtils.FormatInternalDate (request.InternalDate.Value)); + builder.Append ("\" "); + } + + if (request.Annotations != null && request.Annotations.Count > 0) { + ImapUtils.FormatAnnotations (builder, request.Annotations, list, false); + + if (builder[builder.Length - 1] != ' ') + builder.Append (' '); } - return new int[0]; + builder.Append ("%L\r\n"); + list.Add (request.Message); + + var command = builder.ToString (); + var args = list.ToArray (); + + var ic = new ImapCommand (Engine, cancellationToken, null, format, command, args) { + Progress = request.TransferProgress + }; + + Engine.QueueCommand (ic); + + return ic; + } + + UniqueId? ProcessAppendResponse (ImapCommand ic) + { + ProcessResponseCodes (ic, this); + + ic.ThrowIfNotOk ("APPEND"); + + var rc = ic.GetResponseCode (ImapResponseCodeType.AppendUid) as AppendUidResponseCode; + + return rc?.UidSet?[0]; } /// - /// Add a set of labels to the specified messages. + /// Append a message to the folder. /// /// - /// Adds a set of labels to the specified messages. + /// Appends a message to the folder and returns the UniqueId assigned to the message. /// - /// The indexes of the messages. - /// The labels to add. - /// If set to true, no events will be emitted. + /// The UID of the appended message, if available; otherwise, . + /// The formatting options. + /// The append request. /// The cancellation token. /// - /// is null. - /// -or- - /// is null. - /// - /// - /// One or more of the is invalid. + /// is . /// -or- - /// No labels were specified. + /// is . /// /// /// The has been disposed. @@ -8277,12 +4506,20 @@ IList ModifyLabels (IList indexes, ulong? modseq, IList labels /// /// The is not authenticated. /// - /// - /// The is not currently open in read-write mode. + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. /// /// /// The operation was canceled via the cancellation token. /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// -or- + /// The request included annotations but the folder does not support annotations. + /// /// /// An I/O error occurred. /// @@ -8292,36 +4529,29 @@ IList ModifyLabels (IList indexes, ulong? modseq, IList labels /// /// The server replied with a NO or BAD response. /// - public override void AddLabels (IList indexes, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + public override UniqueId? Append (FormatOptions options, IAppendRequest request, CancellationToken cancellationToken = default) { - if (labels == null) - throw new ArgumentNullException (nameof (labels)); + var ic = QueueAppendCommand (options, request, cancellationToken); - if (labels.Count == 0) - throw new ArgumentException ("No labels were specified.", nameof (labels)); + Engine.Run (ic); - ModifyLabels (indexes, null, labels, silent ? "+X-GM-LABELS.SILENT" : "+X-GM-LABELS", cancellationToken); + return ProcessAppendResponse (ic); } /// - /// Remove a set of labels from the specified messages. + /// Asynchronously append a message to the folder. /// /// - /// Removes a set of labels from the specified messages. + /// Asynchronously appends a message to the folder and returns the UniqueId assigned to the message. /// - /// The indexes of the messages. - /// The labels to remove. - /// If set to true, no events will be emitted. + /// The UID of the appended message, if available; otherwise, . + /// The formatting options. + /// The append request. /// The cancellation token. /// - /// is null. - /// -or- - /// is null. - /// - /// - /// One or more of the is invalid. + /// is . /// -or- - /// No labels were specified. + /// is . /// /// /// The has been disposed. @@ -8332,12 +4562,20 @@ IList ModifyLabels (IList indexes, ulong? modseq, IList labels /// /// The is not authenticated. /// - /// - /// The is not currently open in read-write mode. + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. /// /// /// The operation was canceled via the cancellation token. /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// -or- + /// The request included annotations but the folder does not support annotations. + /// /// /// An I/O error occurred. /// @@ -8347,34 +4585,123 @@ IList ModifyLabels (IList indexes, ulong? modseq, IList labels /// /// The server replied with a NO or BAD response. /// - public override void RemoveLabels (IList indexes, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + public override async Task AppendAsync (FormatOptions options, IAppendRequest request, CancellationToken cancellationToken = default) + { + var ic = QueueAppendCommand (options, request, cancellationToken); + + await Engine.RunAsync (ic).ConfigureAwait (false); + + return ProcessAppendResponse (ic); + } + + void ValidateArguments (FormatOptions options, IList requests) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (requests == null) + throw new ArgumentNullException (nameof (requests)); + + for (int i = 0; i < requests.Count; i++) { + if (requests[i] == null) + throw new ArgumentException ("One or more of the requests is null."); + + var annotations = requests[i].Annotations; + if (annotations != null && annotations.Count > 0 && (Engine.Capabilities & ImapCapabilities.Annotate) == 0) + throw new NotSupportedException ("One ore more requests included annotations but the IMAP server does not support annotations."); + } + + CheckState (false, false); + } + + ImapCommand QueueMultiAppendCommand (FormatOptions options, IList requests, CancellationToken cancellationToken) + { + var format = CreateAppendOptions (options); + var builder = new StringBuilder ("APPEND %F"); + var list = new List { + this + }; + + for (int i = 0; i < requests.Count; i++) { + var keywords = requests[i].Keywords; + int numKeywords = keywords != null ? keywords.Count : 0; + + builder.Append (' '); + + if ((requests[i].Flags & SettableFlags) != 0 || numKeywords > 0) { + ImapUtils.FormatFlagsList (builder, requests[i].Flags, numKeywords); + builder.Append (' '); + } + + if (keywords != null) { + foreach (var keyword in keywords) + list.Add (keyword); + } + + var internalDate = requests[i].InternalDate; + if (internalDate.HasValue) { + builder.Append ('"'); + builder.Append (ImapUtils.FormatInternalDate (internalDate.Value)); + builder.Append ("\" "); + } + + var annotations = requests[i].Annotations; + if (annotations != null && annotations.Count > 0) { + ImapUtils.FormatAnnotations (builder, annotations, list, false); + + if (builder[builder.Length - 1] != ' ') + builder.Append (' '); + } + + builder.Append ("%L"); + list.Add (requests[i].Message); + } + + builder.Append ("\r\n"); + + var command = builder.ToString (); + var args = list.ToArray (); + + var ic = new ImapCommand (Engine, cancellationToken, null, format, command, args) { + Progress = requests[0].TransferProgress + }; + + Engine.QueueCommand (ic); + + return ic; + } + + IList ProcessMultiAppendResponse (ImapCommand ic) { - if (labels == null) - throw new ArgumentNullException (nameof (labels)); + ProcessResponseCodes (ic, this); + + ic.ThrowIfNotOk ("APPEND"); - if (labels.Count == 0) - throw new ArgumentException ("No labels were specified.", nameof (labels)); + var rc = ic.GetResponseCode (ImapResponseCodeType.AppendUid) as AppendUidResponseCode; - ModifyLabels (indexes, null, labels, silent ? "-X-GM-LABELS.SILENT" : "-X-GM-LABELS", cancellationToken); + if (rc != null && rc.UidSet != null) + return rc.UidSet; + + return Array.Empty (); } /// - /// Sets the labels of the specified messages. + /// Append multiple messages to the folder. /// /// - /// Sets the labels of the specified messages. + /// Appends multiple messages to the folder and returns the UniqueIds assigned to the messages. /// - /// The indexes of the messages. - /// The labels to set. - /// If set to true, no events will be emitted. + /// The UIDs of the appended messages, if available; otherwise an empty array. + /// The formatting options. + /// The append requests. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// - /// - /// One or more of the is invalid. + /// + /// One or more of the is . /// /// /// The has been disposed. @@ -8385,12 +4712,20 @@ IList ModifyLabels (IList indexes, ulong? modseq, IList labels /// /// The is not authenticated. /// - /// - /// The is not currently open in read-write mode. + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. /// /// /// The operation was canceled via the cancellation token. /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// -or- + /// One ore more requests included annotations but the folder does not support annotations. + /// /// /// An I/O error occurred. /// @@ -8400,35 +4735,55 @@ IList ModifyLabels (IList indexes, ulong? modseq, IList labels /// /// The server replied with a NO or BAD response. /// - public override void SetLabels (IList indexes, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + public override IList Append (FormatOptions options, IList requests, CancellationToken cancellationToken = default) { - if (labels == null) - throw new ArgumentNullException (nameof (labels)); + ValidateArguments (options, requests); + + if (requests.Count == 0) + return Array.Empty (); + + if ((Engine.Capabilities & ImapCapabilities.MultiAppend) != 0) { + var ic = QueueMultiAppendCommand (options, requests, cancellationToken); + + Engine.Run (ic); + + return ProcessMultiAppendResponse (ic); + } - ModifyLabels (indexes, null, labels, silent ? "X-GM-LABELS.SILENT" : "X-GM-LABELS", cancellationToken); + // FIXME: use an aggregate progress reporter + var uids = new List (); + + for (int i = 0; i < requests.Count; i++) { + var uid = Append (options, requests[i], cancellationToken); + if (uids != null && uid.HasValue) + uids.Add (uid.Value); + else + uids = null; + } + + if (uids == null) + return Array.Empty (); + + return uids; } /// - /// Add a set of labels to the specified messages only if their mod-sequence value is less than the specified value. + /// Asynchronously append multiple messages to the folder. /// /// - /// Adds a set of labels to the specified messages only if their mod-sequence value is less than the specified value. + /// Asynchronously appends multiple messages to the folder and returns the UniqueIds assigned to the messages. /// - /// The indexes of the messages that were not updated. - /// The indexes of the messages. - /// The mod-sequence value. - /// The labels to add. - /// If set to true, no events will be emitted. + /// The UIDs of the appended messages, if available; otherwise an empty array. + /// The formatting options. + /// The append requests. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// - /// - /// One or more of the is invalid. - /// -or- - /// No labels were specified. + /// + /// One or more of the is . /// /// /// The has been disposed. @@ -8439,12 +4794,20 @@ IList ModifyLabels (IList indexes, ulong? modseq, IList labels /// /// The is not authenticated. /// - /// - /// The is not currently open in read-write mode. + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. /// /// /// The operation was canceled via the cancellation token. /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// -or- + /// One ore more requests included annotations but the folder does not support annotations. + /// /// /// An I/O error occurred. /// @@ -8454,38 +4817,136 @@ IList ModifyLabels (IList indexes, ulong? modseq, IList labels /// /// The server replied with a NO or BAD response. /// - public override IList AddLabels (IList indexes, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + public override async Task> AppendAsync (FormatOptions options, IList requests, CancellationToken cancellationToken = default) + { + ValidateArguments (options, requests); + + if (requests.Count == 0) + return Array.Empty (); + + if ((Engine.Capabilities & ImapCapabilities.MultiAppend) != 0) { + var ic = QueueMultiAppendCommand (options, requests, cancellationToken); + + await Engine.RunAsync (ic).ConfigureAwait (false); + + return ProcessMultiAppendResponse (ic); + } + + // FIXME: use an aggregate progress reporter + var uids = new List (); + + for (int i = 0; i < requests.Count; i++) { + var uid = await AppendAsync (options, requests[i], cancellationToken).ConfigureAwait (false); + if (uids != null && uid.HasValue) + uids.Add (uid.Value); + else + uids = null; + } + + if (uids == null) + return Array.Empty (); + + return uids; + } + + void ValidateArguments (FormatOptions options, UniqueId uid, IReplaceRequest request) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (!uid.IsValid) + throw new ArgumentException ("The uid is invalid.", nameof (uid)); + + if (request == null) + throw new ArgumentNullException (nameof (request)); + + if (request.Destination != null && !(request.Destination is ImapFolder target && target.Engine == Engine)) + throw new ArgumentException ("The destination folder does not belong to this ImapClient.", nameof (request)); + + if (request.Annotations != null && request.Annotations.Count > 0 && (Engine.Capabilities & ImapCapabilities.Annotate) == 0) + throw new NotSupportedException ("The IMAP server does not support annotations."); + + CheckState (true, true); + } + + ImapCommand QueueReplaceCommand (FormatOptions options, UniqueId uid, IReplaceRequest request, CancellationToken cancellationToken) + { + var format = CreateAppendOptions (options); + int numKeywords = request.Keywords != null ? request.Keywords.Count : 0; + var builder = new StringBuilder ($"UID REPLACE {uid} %F "); + var list = new List { + request.Destination ?? this + }; + + if ((request.Flags & SettableFlags) != 0 || numKeywords > 0) { + ImapUtils.FormatFlagsList (builder, request.Flags, numKeywords); + builder.Append (' '); + } + + if (request.Keywords != null) { + foreach (var keyword in request.Keywords) + list.Add (keyword); + } + + if (request.InternalDate.HasValue) { + builder.Append ('"'); + builder.Append (ImapUtils.FormatInternalDate (request.InternalDate.Value)); + builder.Append ("\" "); + } + + if (request.Annotations != null && request.Annotations.Count > 0) { + ImapUtils.FormatAnnotations (builder, request.Annotations, list, false); + + if (builder[builder.Length - 1] != ' ') + builder.Append (' '); + } + + builder.Append ("%L\r\n"); + list.Add (request.Message); + + var command = builder.ToString (); + var args = list.ToArray (); + + var ic = new ImapCommand (Engine, cancellationToken, null, format, command, args) { + Progress = request.TransferProgress + }; + + Engine.QueueCommand (ic); + + return ic; + } + + UniqueId? ProcessReplaceResponse (ImapCommand ic) { - if (labels == null) - throw new ArgumentNullException (nameof (labels)); + ProcessResponseCodes (ic, this); - if (labels.Count == 0) - throw new ArgumentException ("No labels were specified.", nameof (labels)); + ic.ThrowIfNotOk ("REPLACE"); - return ModifyLabels (indexes, modseq, labels, silent ? "+X-GM-LABELS.SILENT" : "+X-GM-LABELS", cancellationToken); + var rc = ic.GetResponseCode (ImapResponseCodeType.AppendUid) as AppendUidResponseCode; + + return rc?.UidSet?[0]; } /// - /// Remove a set of labels from the specified messages only if their mod-sequence value is less than the specified value. + /// Replace a message in the folder. /// /// - /// Removes a set of labels from the specified messages only if their mod-sequence value is less than the specified value. + /// Replaces a message in the folder and returns the UniqueId assigned to the new message. /// - /// The indexes of the messages that were not updated. - /// The indexes of the messages. - /// The mod-sequence value. - /// The labels to remove. - /// If set to true, no events will be emitted. + /// The UID of the new message, if available; otherwise, . + /// The formatting options. + /// The UID of the message to be replaced. + /// The replace request. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// - /// One or more of the is invalid. + /// is invalid. /// -or- - /// No labels were specified. + /// The destination folder does not belong to this . /// /// /// The has been disposed. @@ -8496,12 +4957,21 @@ IList ModifyLabels (IList indexes, ulong? modseq, IList labels /// /// The is not authenticated. /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// /// /// The is not currently open in read-write mode. /// /// /// The operation was canceled via the cancellation token. /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// /// /// An I/O error occurred. /// @@ -8511,36 +4981,46 @@ IList ModifyLabels (IList indexes, ulong? modseq, IList labels /// /// The server replied with a NO or BAD response. /// - public override IList RemoveLabels (IList indexes, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + public override UniqueId? Replace (FormatOptions options, UniqueId uid, IReplaceRequest request, CancellationToken cancellationToken = default) { - if (labels == null) - throw new ArgumentNullException (nameof (labels)); + ValidateArguments (options, uid, request); + + if ((Engine.Capabilities & ImapCapabilities.Replace) == 0) { + var destination = request.Destination as ImapFolder ?? this; + var appended = destination.Append (options, request, cancellationToken); + Store (new[] { uid }, AddDeletedFlag, cancellationToken); + if ((Engine.Capabilities & ImapCapabilities.UidPlus) != 0) + Expunge (new[] { uid }, cancellationToken); + return appended; + } + + var ic = QueueReplaceCommand (options, uid, request, cancellationToken); - if (labels.Count == 0) - throw new ArgumentException ("No labels were specified.", nameof (labels)); + Engine.Run (ic); - return ModifyLabels (indexes, modseq, labels, silent ? "-X-GM-LABELS.SILENT" : "-X-GM-LABELS", cancellationToken); + return ProcessReplaceResponse (ic); } /// - /// Set the labels of the specified messages only if their mod-sequence value is less than the specified value. + /// Asynchronously replace a message in the folder. /// /// - /// Sets the labels of the specified messages only if their mod-sequence value is less than the specified value. + /// Asynchronously replaces a message in the folder and returns the UniqueId assigned to the new message. /// - /// The indexes of the messages that were not updated. - /// The indexes of the messages. - /// The mod-sequence value. - /// The labels to set. - /// If set to true, no events will be emitted. + /// The UID of the new message, if available; otherwise, . + /// The formatting options. + /// The UID of the message to be replaced. + /// The replace request. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// - /// One or more of the is invalid. + /// is invalid. + /// -or- + /// The destination folder does not belong to this . /// /// /// The has been disposed. @@ -8551,12 +5031,21 @@ IList ModifyLabels (IList indexes, ulong? modseq, IList labels /// /// The is not authenticated. /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// /// /// The is not currently open in read-write mode. /// /// /// The operation was canceled via the cancellation token. /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// /// /// An I/O error occurred. /// @@ -8566,524 +5055,115 @@ IList ModifyLabels (IList indexes, ulong? modseq, IList labels /// /// The server replied with a NO or BAD response. /// - public override IList SetLabels (IList indexes, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + public override async Task ReplaceAsync (FormatOptions options, UniqueId uid, IReplaceRequest request, CancellationToken cancellationToken = default) { - if (labels == null) - throw new ArgumentNullException (nameof (labels)); - - return ModifyLabels (indexes, modseq, labels, silent ? "X-GM-LABELS.SILENT" : "X-GM-LABELS", cancellationToken); - } + ValidateArguments (options, uid, request); - static bool IsAscii (string text) - { - for (int i = 0; i < text.Length; i++) { - if (text[i] > 127) - return false; + if ((Engine.Capabilities & ImapCapabilities.Replace) == 0) { + var destination = request.Destination as ImapFolder ?? this; + var appended = await destination.AppendAsync (options, request, cancellationToken).ConfigureAwait (false); + await StoreAsync (new[] { uid }, AddDeletedFlag, cancellationToken).ConfigureAwait (false); + if ((Engine.Capabilities & ImapCapabilities.UidPlus) != 0) + await ExpungeAsync (new[] { uid }, cancellationToken).ConfigureAwait (false); + return appended; } - return true; - } + var ic = QueueReplaceCommand (options, uid, request, cancellationToken); - static string FormatDateTime (DateTime date) - { - return date.ToString ("d-MMM-yyyy", CultureInfo.InvariantCulture); + await Engine.RunAsync (ic).ConfigureAwait (false); + + return ProcessReplaceResponse (ic); } - void BuildQuery (StringBuilder builder, SearchQuery query, List args, bool parens, ref bool ascii) + ImapCommand QueueReplaceCommand (FormatOptions options, int index, IReplaceRequest request, CancellationToken cancellationToken) { - TextSearchQuery text = null; - NumericSearchQuery numeric; - FilterSearchQuery filter; - HeaderSearchQuery header; - BinarySearchQuery binary; - UnarySearchQuery unary; - DateSearchQuery date; - UidSearchQuery uid; + var format = CreateAppendOptions (options); + int numKeywords = request.Keywords != null ? request.Keywords.Count : 0; + var builder = new StringBuilder ($"REPLACE %d %F "); + var list = new List { + index + 1, + request.Destination ?? this + }; - switch (query.Term) { - case SearchTerm.All: - builder.Append ("ALL"); - break; - case SearchTerm.And: - binary = (BinarySearchQuery) query; - if (parens) - builder.Append ('('); - BuildQuery (builder, binary.Left, args, false, ref ascii); + if ((request.Flags & SettableFlags) != 0) { + ImapUtils.FormatFlagsList (builder, request.Flags, numKeywords); builder.Append (' '); - BuildQuery (builder, binary.Right, args, false, ref ascii); - if (parens) - builder.Append (')'); - break; - case SearchTerm.Answered: - builder.Append ("ANSWERED"); - break; - case SearchTerm.BccContains: - text = (TextSearchQuery) query; - builder.Append ("BCC %S"); - args.Add (text.Text); - break; - case SearchTerm.BodyContains: - text = (TextSearchQuery) query; - builder.Append ("BODY %S"); - args.Add (text.Text); - break; - case SearchTerm.CcContains: - text = (TextSearchQuery) query; - builder.Append ("CC %S"); - args.Add (text.Text); - break; - case SearchTerm.Deleted: - builder.Append ("DELETED"); - break; - case SearchTerm.DeliveredAfter: - date = (DateSearchQuery) query; - builder.AppendFormat ("SINCE {0}", FormatDateTime (date.Date)); - break; - case SearchTerm.DeliveredBefore: - date = (DateSearchQuery) query; - builder.AppendFormat ("BEFORE {0}", FormatDateTime (date.Date)); - break; - case SearchTerm.DeliveredOn: - date = (DateSearchQuery) query; - builder.AppendFormat ("ON {0}", FormatDateTime (date.Date)); - break; - case SearchTerm.Draft: - builder.Append ("DRAFT"); - break; - case SearchTerm.Filter: - if ((Engine.Capabilities & ImapCapabilities.Filters) == 0) - throw new NotSupportedException ("The FILTER search term is not supported by the IMAP server."); - - filter = (FilterSearchQuery) query; - builder.Append ("FILTER %S"); - args.Add (filter.Name); - break; - case SearchTerm.Flagged: - builder.Append ("FLAGGED"); - break; - case SearchTerm.FromContains: - text = (TextSearchQuery) query; - builder.Append ("FROM %S"); - args.Add (text.Text); - break; - case SearchTerm.Fuzzy: - if ((Engine.Capabilities & ImapCapabilities.FuzzySearch) == 0) - throw new NotSupportedException ("The FUZZY search term is not supported by the IMAP server."); - - builder.Append ("FUZZY "); - unary = (UnarySearchQuery) query; - BuildQuery (builder, unary.Operand, args, true, ref ascii); - break; - case SearchTerm.HeaderContains: - header = (HeaderSearchQuery) query; - builder.AppendFormat ("HEADER {0} %S", header.Field); - args.Add (header.Value); - break; - case SearchTerm.Keyword: - text = (TextSearchQuery) query; - builder.Append ("KEYWORD %S"); - args.Add (text.Text); - break; - case SearchTerm.LargerThan: - numeric = (NumericSearchQuery) query; - builder.AppendFormat ("LARGER {0}", numeric.Value); - break; - case SearchTerm.MessageContains: - text = (TextSearchQuery) query; - builder.Append ("TEXT %S"); - args.Add (text.Text); - break; - case SearchTerm.ModSeq: - numeric = (NumericSearchQuery) query; - builder.AppendFormat ("MODSEQ {0}", numeric.Value); - break; - case SearchTerm.New: - builder.Append ("NEW"); - break; - case SearchTerm.Not: - builder.Append ("NOT "); - unary = (UnarySearchQuery) query; - BuildQuery (builder, unary.Operand, args, true, ref ascii); - break; - case SearchTerm.NotAnswered: - builder.Append ("UNANSWERED"); - break; - case SearchTerm.NotDeleted: - builder.Append ("UNDELETED"); - break; - case SearchTerm.NotDraft: - builder.Append ("UNDRAFT"); - break; - case SearchTerm.NotFlagged: - builder.Append ("UNFLAGGED"); - break; - case SearchTerm.NotKeyword: - text = (TextSearchQuery) query; - builder.Append ("UNKEYWORD %S"); - args.Add (text.Text); - break; - case SearchTerm.NotRecent: - builder.Append ("OLD"); - break; - case SearchTerm.NotSeen: - builder.Append ("UNSEEN"); - break; - case SearchTerm.Older: - if ((Engine.Capabilities & ImapCapabilities.Within) == 0) - throw new NotSupportedException ("The OLDER search term is not supported by the IMAP server."); - - numeric = (NumericSearchQuery) query; - builder.AppendFormat ("OLDER {0}", numeric.Value); - break; - case SearchTerm.Or: - builder.Append ("OR "); - binary = (BinarySearchQuery) query; - BuildQuery (builder, binary.Left, args, true, ref ascii); - builder.Append (' '); - BuildQuery (builder, binary.Right, args, true, ref ascii); - break; - case SearchTerm.Recent: - builder.Append ("RECENT"); - break; - case SearchTerm.Seen: - builder.Append ("SEEN"); - break; - case SearchTerm.SentAfter: - date = (DateSearchQuery) query; - builder.AppendFormat ("SENTSINCE {0}", FormatDateTime (date.Date)); - break; - case SearchTerm.SentBefore: - date = (DateSearchQuery) query; - builder.AppendFormat ("SENTBEFORE {0}", FormatDateTime (date.Date)); - break; - case SearchTerm.SentOn: - date = (DateSearchQuery) query; - builder.AppendFormat ("SENTON {0}", FormatDateTime (date.Date)); - break; - case SearchTerm.SmallerThan: - numeric = (NumericSearchQuery) query; - builder.AppendFormat ("SMALLER {0}", numeric.Value); - break; - case SearchTerm.SubjectContains: - text = (TextSearchQuery) query; - builder.Append ("SUBJECT %S"); - args.Add (text.Text); - break; - case SearchTerm.ToContains: - text = (TextSearchQuery) query; - builder.Append ("TO %S"); - args.Add (text.Text); - break; - case SearchTerm.Uid: - uid = (UidSearchQuery) query; - builder.AppendFormat ("UID {0}", ImapUtils.FormatUidSet (uid.Uids)); - break; - case SearchTerm.Younger: - if ((Engine.Capabilities & ImapCapabilities.Within) == 0) - throw new NotSupportedException ("The YOUNGER search term is not supported by the IMAP server."); - - numeric = (NumericSearchQuery) query; - builder.AppendFormat ("YOUNGER {0}", numeric.Value); - break; - case SearchTerm.GMailMessageId: - if ((Engine.Capabilities & ImapCapabilities.GMailExt1) == 0) - throw new NotSupportedException ("The X-GM-MSGID search term is not supported by the IMAP server."); - - numeric = (NumericSearchQuery) query; - builder.AppendFormat ("X-GM-MSGID {0}", numeric.Value); - break; - case SearchTerm.GMailThreadId: - if ((Engine.Capabilities & ImapCapabilities.GMailExt1) == 0) - throw new NotSupportedException ("The X-GM-THRID search term is not supported by the IMAP server."); - - numeric = (NumericSearchQuery) query; - builder.AppendFormat ("X-GM-THRID {0}", numeric.Value); - break; - case SearchTerm.GMailLabels: - if ((Engine.Capabilities & ImapCapabilities.GMailExt1) == 0) - throw new NotSupportedException ("The X-GM-LABELS search term is not supported by the IMAP server."); - - text = (TextSearchQuery) query; - builder.Append ("X-GM-LABELS %S"); - args.Add (text.Text); - break; - case SearchTerm.GMailRaw: - if ((Engine.Capabilities & ImapCapabilities.GMailExt1) == 0) - throw new NotSupportedException ("The X-GM-RAW search term is not supported by the IMAP server."); - - text = (TextSearchQuery) query; - builder.Append ("X-GM-RAW %S"); - args.Add (text.Text); - break; - default: - throw new ArgumentOutOfRangeException (); } - if (text != null && !IsAscii (text.Text)) - ascii = false; - } - - string BuildQueryExpression (SearchQuery query, List args, out string charset) - { - var builder = new StringBuilder (); - bool ascii = true; - - BuildQuery (builder, query, args, false, ref ascii); - - charset = ascii ? null : "UTF-8"; + if (request.Keywords != null) { + foreach (var keyword in request.Keywords) + list.Add (keyword); + } - return builder.ToString (); - } + if (request.InternalDate.HasValue) { + builder.Append ('"'); + builder.Append (ImapUtils.FormatInternalDate (request.InternalDate.Value)); + builder.Append ("\" "); + } - static string BuildSortOrder (IList orderBy) - { - var builder = new StringBuilder (); + if (request.Annotations != null && request.Annotations.Count > 0) { + ImapUtils.FormatAnnotations (builder, request.Annotations, list, false); - builder.Append ('('); - for (int i = 0; i < orderBy.Count; i++) { - if (builder.Length > 1) + if (builder[builder.Length - 1] != ' ') builder.Append (' '); - - if (orderBy[i].Order == SortOrder.Descending) - builder.Append ("REVERSE "); - - switch (orderBy[i].Type) { - case OrderByType.Arrival: builder.Append ("ARRIVAL"); break; - case OrderByType.Cc: builder.Append ("CC"); break; - case OrderByType.Date: builder.Append ("DATE"); break; - case OrderByType.DisplayFrom: builder.Append ("DISPLAYFROM"); break; - case OrderByType.DisplayTo: builder.Append ("DISPLAYTO"); break; - case OrderByType.From: builder.Append ("FROM"); break; - case OrderByType.Size: builder.Append ("SIZE"); break; - case OrderByType.Subject: builder.Append ("SUBJECT"); break; - case OrderByType.To: builder.Append ("TO"); break; - default: throw new ArgumentOutOfRangeException (); - } } - builder.Append (')'); - - return builder.ToString (); - } - - static void SearchMatches (ImapEngine engine, ImapCommand ic, int index) - { - var uids = new UniqueIdSet (SortOrder.Ascending); - var results = (SearchResults) ic.UserData; - ImapToken token; - ulong modseq; - uint uid; - - do { - token = engine.PeekToken (ic.CancellationToken); - - // keep reading UIDs until we get to the end of the line or until we get a "(MODSEQ ####)" - if (token.Type == ImapTokenType.Eoln || token.Type == ImapTokenType.OpenParen) - break; - - token = engine.ReadToken (ic.CancellationToken); - - if (token.Type != ImapTokenType.Atom || !uint.TryParse ((string) token.Value, out uid) || uid == 0) - throw ImapEngine.UnexpectedToken (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "SEARCH", token); - - uids.Add (new UniqueId (ic.Folder.UidValidity, uid)); - } while (true); - if (token.Type == ImapTokenType.OpenParen) { - engine.ReadToken (ic.CancellationToken); - - do { - token = engine.ReadToken (ic.CancellationToken); - - if (token.Type == ImapTokenType.CloseParen) - break; - - if (token.Type != ImapTokenType.Atom) - throw ImapEngine.UnexpectedToken (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "SEARCH", token); - - var atom = (string) token.Value; + builder.Append ("%L\r\n"); + list.Add (request.Message); - switch (atom) { - case "MODSEQ": - token = engine.ReadToken (ic.CancellationToken); + var command = builder.ToString (); + var args = list.ToArray (); - if (token.Type != ImapTokenType.Atom || !ulong.TryParse ((string) token.Value, out modseq)) { - Debug.WriteLine ("Expected 64-bit nz-number as the MODSEQ value, but got: {0}", token); - throw ImapEngine.UnexpectedToken (ImapEngine.GenericItemSyntaxErrorFormat, atom, token); - } - break; - } + var ic = new ImapCommand (Engine, cancellationToken, null, format, command, args) { + Progress = request.TransferProgress + }; - token = engine.PeekToken (ic.CancellationToken); - } while (token.Type != ImapTokenType.Eoln); - } + Engine.QueueCommand (ic); - results.UniqueIds = uids; + return ic; } - static void ESearchMatches (ImapEngine engine, ImapCommand ic, int index) + void ValidateArguments (FormatOptions options, int index, IReplaceRequest request) { - var token = engine.ReadToken (ic.CancellationToken); - var results = (SearchResults) ic.UserData; - UniqueIdSet uids = null; - int parenDepth = 0; - //bool uid = false; - uint min, max; - ulong modseq; - string atom; - string tag; - int count; - - if (token.Type == ImapTokenType.OpenParen) { - // optional search correlator - do { - token = engine.ReadToken (ic.CancellationToken); - - if (token.Type == ImapTokenType.CloseParen) - break; - - if (token.Type != ImapTokenType.Atom) - throw ImapEngine.UnexpectedToken (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); - - atom = (string) token.Value; - - if (atom == "TAG") { - token = engine.ReadToken (ic.CancellationToken); - - if (token.Type != ImapTokenType.Atom && token.Type != ImapTokenType.QString) - throw ImapEngine.UnexpectedToken (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); - - tag = (string) token.Value; - - if (tag != ic.Tag) - throw new ImapProtocolException ("Unexpected TAG value in untagged ESEARCH response: " + tag); - } - } while (true); - - token = engine.ReadToken (ic.CancellationToken); - } - - if (token.Type == ImapTokenType.Atom && ((string) token.Value) == "UID") { - token = engine.ReadToken (ic.CancellationToken); - //uid = true; - } - - do { - if (token.Type == ImapTokenType.CloseParen) { - if (parenDepth == 0) - throw ImapEngine.UnexpectedToken (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); - - token = engine.ReadToken (ic.CancellationToken); - parenDepth--; - } - - if (token.Type == ImapTokenType.Eoln) { - // unget the eoln token - engine.Stream.UngetToken (token); - break; - } - - if (token.Type == ImapTokenType.OpenParen) { - token = engine.ReadToken (ic.CancellationToken); - parenDepth++; - } - - if (token.Type != ImapTokenType.Atom) - throw ImapEngine.UnexpectedToken (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); - - atom = (string) token.Value; - - token = engine.ReadToken (ic.CancellationToken); - - switch (atom) { - case "RELEVANCY": - if (token.Type != ImapTokenType.OpenParen) - throw ImapEngine.UnexpectedToken (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); - - results.Relevancy = new List (); - - do { - int score; - - token = engine.ReadToken (ic.CancellationToken); - - if (token.Type == ImapTokenType.CloseParen) - break; - - if (token.Type != ImapTokenType.Atom || !int.TryParse ((string) token.Value, out score) || score < 1 || score > 100) - throw ImapEngine.UnexpectedToken (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); - - results.Relevancy.Add ((byte) score); - } while (true); - break; - case "MODSEQ": - if (token.Type != ImapTokenType.Atom) - throw ImapEngine.UnexpectedToken (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); - - if (!ulong.TryParse ((string) token.Value, out modseq)) - throw ImapEngine.UnexpectedToken (ImapEngine.GenericItemSyntaxErrorFormat, atom, token); - - results.ModSeq = modseq; - break; - case "COUNT": - if (token.Type != ImapTokenType.Atom) - throw ImapEngine.UnexpectedToken (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); - - if (!int.TryParse ((string) token.Value, out count)) - throw ImapEngine.UnexpectedToken (ImapEngine.GenericItemSyntaxErrorFormat, atom, token); - - results.Count = count; - break; - case "MIN": - if (!uint.TryParse ((string) token.Value, out min) || min == 0) - throw ImapEngine.UnexpectedToken (ImapEngine.GenericItemSyntaxErrorFormat, atom, token); - - results.Min = new UniqueId (ic.Folder.UidValidity, min); - break; - case "MAX": - if (token.Type != ImapTokenType.Atom) - throw ImapEngine.UnexpectedToken (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); - - if (!uint.TryParse ((string) token.Value, out max) || max == 0) - throw ImapEngine.UnexpectedToken (ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + if (options == null) + throw new ArgumentNullException (nameof (options)); - results.Max = new UniqueId (ic.Folder.UidValidity, max); - break; - case "ALL": - if (token.Type != ImapTokenType.Atom) - throw ImapEngine.UnexpectedToken (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException (nameof (index)); - if (!UniqueIdSet.TryParse ((string) token.Value, ic.Folder.UidValidity, out uids)) - throw ImapEngine.UnexpectedToken (ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + if (request == null) + throw new ArgumentNullException (nameof (request)); - results.Count = uids.Count; - break; - default: - throw ImapEngine.UnexpectedToken (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); - } + if (request.Destination != null && !(request.Destination is ImapFolder target && target.Engine == Engine)) + throw new ArgumentException ("The destination folder does not belong to this ImapClient.", nameof (request)); - token = engine.ReadToken (ic.CancellationToken); - } while (true); + if (request.Annotations != null && request.Annotations.Count > 0 && (Engine.Capabilities & ImapCapabilities.Annotate) == 0) + throw new NotSupportedException ("The IMAP server does not support annotations."); - results.UniqueIds = uids ?? new UniqueIdSet (); + CheckState (true, true); } /// - /// Searches the folder for messages matching the specified query. + /// Replace a message in the folder. /// /// - /// Sends a UID SEARCH command with the specified query passed directly to the IMAP server - /// with no interpretation by MailKit. This means that the query may contain any arguments that a - /// UID SEARCH command is allowed to have according to the IMAP specifications and any - /// extensions that are supported, including RETURN parameters. + /// Replaces a message in the folder and returns the UniqueId assigned to the new message. /// - /// An array of matching UIDs. - /// The search query. + /// The UID of the new message, if available; otherwise, . + /// The formatting options. + /// The index of the message to be replaced. + /// The replace request. /// The cancellation token. /// - /// is null. + /// is . + /// -or- + /// is . + /// + /// + /// is out of range. /// /// - /// is an empty string. + /// The destination folder does not belong to this . /// /// /// The has been disposed. @@ -9094,12 +5174,21 @@ static void ESearchMatches (ImapEngine engine, ImapCommand ic, int index) /// /// The is not authenticated. /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// /// - /// The is not currently open. + /// The is not currently open in read-write mode. /// /// /// The operation was canceled via the cancellation token. /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// /// /// An I/O error occurred. /// @@ -9109,53 +5198,45 @@ static void ESearchMatches (ImapEngine engine, ImapCommand ic, int index) /// /// The server replied with a NO or BAD response. /// - public SearchResults Search (string query, CancellationToken cancellationToken = default (CancellationToken)) + public override UniqueId? Replace (FormatOptions options, int index, IReplaceRequest request, CancellationToken cancellationToken = default) { - if (query == null) - throw new ArgumentNullException (nameof (query)); - - query = query.Trim (); - - if (query.Length == 0) - throw new ArgumentException ("Cannot search using an empty query.", nameof (query)); - - CheckState (true, false); - - var command = "UID SEARCH " + query + "\r\n"; - var ic = new ImapCommand (Engine, cancellationToken, this, command); - if ((Engine.Capabilities & ImapCapabilities.ESearch) != 0) - ic.RegisterUntaggedHandler ("ESEARCH", ESearchMatches); - ic.RegisterUntaggedHandler ("SEARCH", SearchMatches); - ic.UserData = new SearchResults (); + ValidateArguments (options, index, request); - Engine.QueueCommand (ic); - Engine.Wait (ic); + if ((Engine.Capabilities & ImapCapabilities.Replace) == 0) { + var destination = request.Destination as ImapFolder ?? this; + var uid = destination.Append (options, request, cancellationToken); + Store (new[] { index }, AddDeletedFlag, cancellationToken); + return uid; + } - ProcessResponseCodes (ic, null); + var ic = QueueReplaceCommand (options, index, request, cancellationToken); - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("SEARCH", ic); + Engine.Run (ic); - return (SearchResults) ic.UserData; + return ProcessReplaceResponse (ic); } /// - /// Asynchronously searches the folder for messages matching the specified query. + /// Asynchronously replace a message in the folder. /// /// - /// Sends a UID SEARCH command with the specified query passed directly to the IMAP server - /// with no interpretation by MailKit. This means that the query may contain any arguments that a - /// UID SEARCH command is allowed to have according to the IMAP specifications and any - /// extensions that are supported, including RETURN parameters. + /// Asynchronously replaces a message in the folder and returns the UniqueId assigned to the new message. /// - /// An array of matching UIDs. - /// The search query. + /// The UID of the new message, if available; otherwise, . + /// The formatting options. + /// The index of the message to be replaced. + /// The replace request. /// The cancellation token. /// - /// is null. + /// is . + /// -or- + /// is . + /// + /// + /// is out of range. /// /// - /// is an empty string. + /// The destination folder does not belong to this . /// /// /// The has been disposed. @@ -9163,15 +5244,24 @@ static void ESearchMatches (ImapEngine engine, ImapCommand ic, int index) /// /// The is not connected. /// - /// - /// The is not authenticated. + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. /// /// - /// The is not currently open. + /// The is not currently open in read-write mode. /// /// /// The operation was canceled via the cancellation token. /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// /// /// An I/O error occurred. /// @@ -9181,30 +5271,117 @@ static void ESearchMatches (ImapEngine engine, ImapCommand ic, int index) /// /// The server replied with a NO or BAD response. /// - public Task SearchAsync (string query, CancellationToken cancellationToken = default (CancellationToken)) + public override async Task ReplaceAsync (FormatOptions options, int index, IReplaceRequest request, CancellationToken cancellationToken = default) + { + ValidateArguments (options, index, request); + + if ((Engine.Capabilities & ImapCapabilities.Replace) == 0) { + var destination = request.Destination as ImapFolder ?? this; + var uid = await destination.AppendAsync (options, request, cancellationToken).ConfigureAwait (false); + await StoreAsync (new[] { index }, AddDeletedFlag, cancellationToken).ConfigureAwait (false); + return uid; + } + + var ic = QueueReplaceCommand (options, index, request, cancellationToken); + + await Engine.RunAsync (ic).ConfigureAwait (false); + + return ProcessReplaceResponse (ic); + } + + ImapCommand QueueGetIndexesCommand (IList uids, CancellationToken cancellationToken) + { + var command = string.Format ("SEARCH UID {0}\r\n", UniqueIdSet.ToString (uids)); + var ic = new ImapCommand (Engine, cancellationToken, this, command); + + if ((Engine.Capabilities & ImapCapabilities.ESearch) != 0) + ic.RegisterUntaggedHandler ("ESEARCH", UntaggedESearchHandler); + + ic.RegisterUntaggedHandler ("SEARCH", UntaggedSearchHandler); + ic.UserData = new SearchResults (SortOrder.Ascending); + + Engine.QueueCommand (ic); + + return ic; + } + + IList ProcessGetIndexesResponse (ImapCommand ic) + { + var results = ProcessSearchResponse (ic); + var indexes = new int[results.UniqueIds.Count]; + for (int i = 0; i < indexes.Length; i++) + indexes[i] = (int) results.UniqueIds[i].Id - 1; + + return indexes; + } + + IList GetIndexes (IList uids, CancellationToken cancellationToken) + { + var ic = QueueGetIndexesCommand (uids, cancellationToken); + + Engine.Run (ic); + + return ProcessGetIndexesResponse (ic); + } + + async Task> GetIndexesAsync (IList uids, CancellationToken cancellationToken) + { + var ic = QueueGetIndexesCommand (uids, cancellationToken); + + await Engine.RunAsync (ic).ConfigureAwait (false); + + return ProcessGetIndexesResponse (ic); + } + + void ValidateArguments (IList uids, IMailFolder destination) + { + if (uids == null) + throw new ArgumentNullException (nameof (uids)); + + CheckValidDestination (destination); + } + + static void GetCopiedUids (ImapCommand ic, ref UniqueIdSet? src, ref UniqueIdSet? dest) { - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return Search (query, cancellationToken); + var rc = ic.GetResponseCode (ImapResponseCodeType.CopyUid); + + if (rc is CopyUidResponseCode copy && copy.SrcUidSet != null && copy.DestUidSet != null) { + if (dest == null) { + dest = copy.DestUidSet; + src = copy.SrcUidSet; + } else { + dest.AddRange (copy.DestUidSet); + src!.AddRange (copy.SrcUidSet); } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); + } + } + + void ProcessCopyToResponse (ImapCommand ic, IMailFolder destination, ref UniqueIdSet? src, ref UniqueIdSet? dest) + { + ProcessCopyToResponse (ic, destination); + + GetCopiedUids (ic, ref src, ref dest); } /// - /// Searches the folder for messages matching the specified query. + /// Copy the specified messages to the destination folder. /// /// - /// The returned array of unique identifiers can be used with methods such as - /// . + /// Copies the specified messages to the destination folder. /// - /// An array of matching UIDs. - /// The search query. + /// The UID mapping of the messages in the destination folder, if available; otherwise an empty mapping. + /// The UIDs of the messages to copy. + /// The destination folder. /// The cancellation token. /// - /// is null. + /// is . + /// -or- + /// is . /// - /// - /// One or more search terms in the are not supported by the IMAP server. + /// + /// One or more of the is invalid. + /// -or- + /// The destination folder does not belong to the . /// /// /// The has been disposed. @@ -9215,9 +5392,15 @@ static void ESearchMatches (ImapEngine engine, ImapCommand ic, int index) /// /// The is not authenticated. /// + /// + /// does not exist. + /// /// /// The is not currently open. /// + /// + /// The IMAP server does not support the UIDPLUS extension. + /// /// /// The operation was canceled via the cancellation token. /// @@ -9230,67 +5413,55 @@ static void ESearchMatches (ImapEngine engine, ImapCommand ic, int index) /// /// The server replied with a NO or BAD response. /// - public override IList Search (SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)) + public override UniqueIdMap CopyTo (IList uids, IMailFolder destination, CancellationToken cancellationToken = default) { - var args = new List (); - string charset; - - if (query == null) - throw new ArgumentNullException (nameof (query)); + ValidateArguments (uids, destination); CheckState (true, false); - var optimized = query.Optimize (new ImapSearchQueryOptimizer ()); - var expr = BuildQueryExpression (optimized, args, out charset); - var command = "UID SEARCH "; - - if ((Engine.Capabilities & ImapCapabilities.ESearch) != 0) - command += "RETURN () "; - - if (charset != null && args.Count > 0 && !Engine.UTF8Enabled) - command += "CHARSET " + charset + " "; - - command += expr + "\r\n"; + if (uids.Count == 0) + return UniqueIdMap.Empty; - var ic = new ImapCommand (Engine, cancellationToken, this, command, args.ToArray ()); - if ((Engine.Capabilities & ImapCapabilities.ESearch) != 0) - ic.RegisterUntaggedHandler ("ESEARCH", ESearchMatches); + if ((Engine.Capabilities & ImapCapabilities.UidPlus) == 0) { + var indexes = GetIndexes (uids, cancellationToken); + CopyTo (indexes, destination, cancellationToken); + return UniqueIdMap.Empty; + } - // Note: always register the untagged SEARCH handler because some servers will brokenly - // respond with "* SEARCH ..." instead of "* ESEARCH ..." even when using the extended - // search syntax. - ic.RegisterUntaggedHandler ("SEARCH", SearchMatches); - ic.UserData = new SearchResults (); + UniqueIdSet? dest = null; + UniqueIdSet? src = null; - Engine.QueueCommand (ic); - Engine.Wait (ic); + foreach (var ic in Engine.QueueCommands (cancellationToken, this, "UID COPY %s %F\r\n", uids, destination)) { + Engine.Run (ic); - ProcessResponseCodes (ic, null); + ProcessCopyToResponse (ic, destination, ref src, ref dest); + } - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("SEARCH", ic); + if (src == null || dest == null) + return UniqueIdMap.Empty; - return ((SearchResults) ic.UserData).UniqueIds; + return new UniqueIdMap (src, dest); } /// - /// Searches the folder for messages matching the specified query. + /// Asynchronously copy the specified messages to the destination folder. /// /// - /// Searches the folder for messages matching the specified query, - /// returning only the specified search results. + /// Copies the specified messages to the destination folder. /// - /// The search results. - /// The search options. - /// The search query. + /// The UID mapping of the messages in the destination folder, if available; otherwise an empty mapping. + /// The UIDs of the messages to copy. + /// The destination folder. /// The cancellation token. /// - /// is null. + /// is . + /// -or- + /// is . /// - /// - /// One or more search terms in the are not supported by the IMAP server. + /// + /// One or more of the is invalid. /// -or- - /// The IMAP server does not support the ESEARCH extension. + /// The destination folder does not belong to the . /// /// /// The has been disposed. @@ -9301,9 +5472,15 @@ static void ESearchMatches (ImapEngine engine, ImapCommand ic, int index) /// /// The is not authenticated. /// + /// + /// does not exist. + /// /// /// The is not currently open. /// + /// + /// The IMAP server does not support the UIDPLUS extension. + /// /// /// The operation was canceled via the cancellation token. /// @@ -9316,78 +5493,73 @@ static void ESearchMatches (ImapEngine engine, ImapCommand ic, int index) /// /// The server replied with a NO or BAD response. /// - public override SearchResults Search (SearchOptions options, SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)) + public override async Task CopyToAsync (IList uids, IMailFolder destination, CancellationToken cancellationToken = default) { - var args = new List (); - string charset; - - if (query == null) - throw new ArgumentNullException (nameof (query)); + ValidateArguments (uids, destination); CheckState (true, false); - if ((Engine.Capabilities & ImapCapabilities.ESearch) == 0) - throw new NotSupportedException ("The IMAP server does not support the ESEARCH extension."); - - var optimized = query.Optimize (new ImapSearchQueryOptimizer ()); - var expr = BuildQueryExpression (optimized, args, out charset); - var command = "UID SEARCH RETURN ("; - - if (options != SearchOptions.All && options != 0) { - if ((options & SearchOptions.All) != 0) - command += "ALL "; - if ((options & SearchOptions.Relevancy) != 0) - command += "RELEVANCY "; - if ((options & SearchOptions.Count) != 0) - command += "COUNT "; - if ((options & SearchOptions.Min) != 0) - command += "MIN "; - if ((options & SearchOptions.Max) != 0) - command += "MAX "; - command = command.TrimEnd (); + if (uids.Count == 0) + return UniqueIdMap.Empty; + + if ((Engine.Capabilities & ImapCapabilities.UidPlus) == 0) { + var indexes = await GetIndexesAsync (uids, cancellationToken).ConfigureAwait (false); + await CopyToAsync (indexes, destination, cancellationToken).ConfigureAwait (false); + return UniqueIdMap.Empty; } - command += ") "; - if (charset != null && args.Count > 0 && !Engine.UTF8Enabled) - command += "CHARSET " + charset + " "; + UniqueIdSet? dest = null; + UniqueIdSet? src = null; - command += expr + "\r\n"; + foreach (var ic in Engine.QueueCommands (cancellationToken, this, "UID COPY %s %F\r\n", uids, destination)) { + await Engine.RunAsync (ic).ConfigureAwait (false); - var ic = new ImapCommand (Engine, cancellationToken, this, command, args.ToArray ()); - ic.RegisterUntaggedHandler ("ESEARCH", ESearchMatches); - ic.UserData = new SearchResults (); + ProcessCopyToResponse (ic, destination, ref src, ref dest); + } - Engine.QueueCommand (ic); - Engine.Wait (ic); + if (src == null || dest == null) + return UniqueIdMap.Empty; - ProcessResponseCodes (ic, null); + return new UniqueIdMap (src, dest); + } - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("SEARCH", ic); + void ProcessMoveToResponse (ImapCommand ic, IMailFolder destination, ref UniqueIdSet? src, ref UniqueIdSet? dest) + { + ProcessMoveToResponse (ic, destination); - return (SearchResults) ic.UserData; + GetCopiedUids (ic, ref src, ref dest); } /// - /// Sort messages matching the specified query. + /// Move the specified messages to the destination folder. /// /// - /// Sends a UID SORT command with the specified query passed directly to the IMAP server - /// with no interpretation by MailKit. This means that the query may contain any arguments that a - /// UID SORT command is allowed to have according to the IMAP specifications and any - /// extensions that are supported, including RETURN parameters. + /// Moves the specified messages to the destination folder. + /// If the IMAP server supports the MOVE extension (check the + /// property for the flag), then this operation will be atomic. + /// Otherwise, MailKit implements this by first copying the messages to the destination folder, then + /// marking them for deletion in the originating folder, and finally expunging them (see + /// for more information about how a + /// subset of messages are expunged). Since the server could disconnect at any point between those 3 + /// (or more) commands, it is advisable for clients to implement their own logic for moving messages when + /// the IMAP server does not support the MOVE command in order to better handle spontaneous server + /// disconnects and other error conditions. /// - /// An array of matching UIDs. - /// The search query. + /// The UID mapping of the messages in the destination folder, if available; otherwise an empty mapping. + /// The UIDs of the messages to move. + /// The destination folder. /// The cancellation token. /// - /// is null. + /// is . + /// -or- + /// is . /// /// - /// is an empty string. - /// - /// - /// The IMAP server does not support the SORT extension. + /// is empty. + /// -or- + /// One or more of the is invalid. + /// -or- + /// The destination folder does not belong to the . /// /// /// The has been disposed. @@ -9398,8 +5570,11 @@ static void ESearchMatches (ImapEngine engine, ImapCommand ic, int index) /// /// The is not authenticated. /// + /// + /// does not exist. + /// /// - /// The is not currently open. + /// The is not currently open in read-write mode. /// /// /// The operation was canceled via the cancellation token. @@ -9413,59 +5588,73 @@ static void ESearchMatches (ImapEngine engine, ImapCommand ic, int index) /// /// The server replied with a NO or BAD response. /// - public SearchResults Sort (string query, CancellationToken cancellationToken = default (CancellationToken)) + public override UniqueIdMap MoveTo (IList uids, IMailFolder destination, CancellationToken cancellationToken = default) { - if (query == null) - throw new ArgumentNullException (nameof (query)); + if ((Engine.Capabilities & ImapCapabilities.Move) == 0) { + var copied = CopyTo (uids, destination, cancellationToken); + Store (uids, AddDeletedFlag, cancellationToken); + Expunge (uids, cancellationToken); + return copied; + } - query = query.Trim (); + if ((Engine.Capabilities & ImapCapabilities.UidPlus) == 0) { + var indexes = GetIndexes (uids, cancellationToken); + MoveTo (indexes, destination, cancellationToken); + return UniqueIdMap.Empty; + } - if (query.Length == 0) - throw new ArgumentException ("Cannot sort using an empty query.", nameof (query)); + ValidateArguments (uids, destination); - if ((Engine.Capabilities & ImapCapabilities.Sort) == 0) - throw new NotSupportedException ("The IMAP server does not support the SORT extension."); + CheckState (true, true); - CheckState (true, false); + if (uids.Count == 0) + return UniqueIdMap.Empty; - var command = "UID SORT " + query + "\r\n"; - var ic = new ImapCommand (Engine, cancellationToken, this, command); - if ((Engine.Capabilities & ImapCapabilities.ESort) != 0) - ic.RegisterUntaggedHandler ("ESEARCH", ESearchMatches); - ic.RegisterUntaggedHandler ("SORT", SearchMatches); - ic.UserData = new SearchResults (); + UniqueIdSet? dest = null; + UniqueIdSet? src = null; - Engine.QueueCommand (ic); - Engine.Wait (ic); + foreach (var ic in Engine.QueueCommands (cancellationToken, this, "UID MOVE %s %F\r\n", uids, destination)) { + Engine.Run (ic); - ProcessResponseCodes (ic, null); + ProcessMoveToResponse (ic, destination, ref src, ref dest); + } - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("SORT", ic); + if (dest == null) + return UniqueIdMap.Empty; - return (SearchResults)ic.UserData; + return new UniqueIdMap (src!, dest); } /// - /// Asynchronously sort messages matching the specified query. + /// Asynchronously move the specified messages to the destination folder. /// /// - /// Sends a UID SORT command with the specified query passed directly to the IMAP server - /// with no interpretation by MailKit. This means that the query may contain any arguments that a - /// UID SORT command is allowed to have according to the IMAP specifications and any - /// extensions that are supported, including RETURN parameters. + /// Moves the specified messages to the destination folder. + /// If the IMAP server supports the MOVE extension (check the + /// property for the flag), then this operation will be atomic. + /// Otherwise, MailKit implements this by first copying the messages to the destination folder, then + /// marking them for deletion in the originating folder, and finally expunging them (see + /// for more information about how a + /// subset of messages are expunged). Since the server could disconnect at any point between those 3 + /// (or more) commands, it is advisable for clients to implement their own logic for moving messages when + /// the IMAP server does not support the MOVE command in order to better handle spontaneous server + /// disconnects and other error conditions. /// - /// An array of matching UIDs. - /// The search query. + /// The UID mapping of the messages in the destination folder, if available; otherwise an empty mapping. + /// The UIDs of the messages to move. + /// The destination folder. /// The cancellation token. /// - /// is null. + /// is . + /// -or- + /// is . /// /// - /// is an empty string. - /// - /// - /// The IMAP server does not support the SORT extension. + /// is empty. + /// -or- + /// One or more of the is invalid. + /// -or- + /// The destination folder does not belong to the . /// /// /// The has been disposed. @@ -9476,8 +5665,11 @@ static void ESearchMatches (ImapEngine engine, ImapCommand ic, int index) /// /// The is not authenticated. /// + /// + /// does not exist. + /// /// - /// The is not currently open. + /// The is not currently open in read-write mode. /// /// /// The operation was canceled via the cancellation token. @@ -9491,48 +5683,109 @@ static void ESearchMatches (ImapEngine engine, ImapCommand ic, int index) /// /// The server replied with a NO or BAD response. /// - public Task SortAsync (string query, CancellationToken cancellationToken = default (CancellationToken)) + public override async Task MoveToAsync (IList uids, IMailFolder destination, CancellationToken cancellationToken = default) { - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return Sort (query, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); + if ((Engine.Capabilities & ImapCapabilities.Move) == 0) { + var copied = await CopyToAsync (uids, destination, cancellationToken).ConfigureAwait (false); + await StoreAsync (uids, AddDeletedFlag, cancellationToken).ConfigureAwait (false); + await ExpungeAsync (uids, cancellationToken).ConfigureAwait (false); + return copied; + } + + if ((Engine.Capabilities & ImapCapabilities.UidPlus) == 0) { + var indexes = await GetIndexesAsync (uids, cancellationToken).ConfigureAwait (false); + await MoveToAsync (indexes, destination, cancellationToken).ConfigureAwait (false); + return UniqueIdMap.Empty; + } + + ValidateArguments (uids, destination); + + CheckState (true, true); + + if (uids.Count == 0) + return UniqueIdMap.Empty; + + UniqueIdSet? dest = null; + UniqueIdSet? src = null; + + foreach (var ic in Engine.QueueCommands (cancellationToken, this, "UID MOVE %s %F\r\n", uids, destination)) { + await Engine.RunAsync (ic).ConfigureAwait (false); + + ProcessMoveToResponse (ic, destination, ref src, ref dest); + } + + if (dest == null) + return UniqueIdMap.Empty; + + return new UniqueIdMap (src!, dest); + } + + void ValidateArguments (IList indexes, IMailFolder destination) + { + if (indexes == null) + throw new ArgumentNullException (nameof (indexes)); + + CheckValidDestination (destination); + } + + ImapCommand? QueueCopyToCommand (IList indexes, IMailFolder destination, CancellationToken cancellationToken) + { + ValidateArguments (indexes, destination); + + CheckState (true, false); + CheckAllowIndexes (); + + if (indexes.Count == 0) + return null; + + var command = new StringBuilder ("COPY "); + ImapUtils.FormatIndexSet (Engine, command, indexes); + command.Append (" %F\r\n"); + + return Engine.QueueCommand (cancellationToken, this, command.ToString (), destination); + } + + void ProcessCopyToResponse (ImapCommand ic, IMailFolder destination) + { + ProcessResponseCodes (ic, destination); + + ic.ThrowIfNotOk ("COPY"); } /// - /// Sort messages matching the specified query. + /// Copy the specified messages to the destination folder. /// /// - /// The returned array of unique identifiers will be sorted in the preferred order and - /// can be used with . + /// Copies the specified messages to the destination folder. /// - /// An array of matching UIDs in the specified sort order. - /// The search query. - /// The sort order. + /// The indexes of the messages to copy. + /// The destination folder. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// - /// is empty. - /// - /// - /// One or more search terms in the are not supported by the IMAP server. + /// One or more of the is invalid. /// -or- - /// The server does not support the SORT extension. + /// The destination folder does not belong to the . /// /// /// The has been disposed. /// + /// + /// The is not currently open. + /// /// /// The is not connected. /// /// /// The is not authenticated. /// + /// + /// does not exist. + /// /// /// The is not currently open. /// @@ -9548,93 +5801,53 @@ static void ESearchMatches (ImapEngine engine, ImapCommand ic, int index) /// /// The server replied with a NO or BAD response. /// - public override IList Sort (SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)) + public override void CopyTo (IList indexes, IMailFolder destination, CancellationToken cancellationToken = default) { - var args = new List (); - string charset; - - if (query == null) - throw new ArgumentNullException (nameof (query)); - - if (orderBy == null) - throw new ArgumentNullException (nameof (orderBy)); - - if (orderBy.Count == 0) - throw new ArgumentException ("No sort order provided.", nameof (orderBy)); - - CheckState (true, false); + var ic = QueueCopyToCommand (indexes, destination, cancellationToken); - if ((Engine.Capabilities & ImapCapabilities.Sort) == 0) - throw new NotSupportedException ("The IMAP server does not support the SORT extension."); - - if ((Engine.Capabilities & ImapCapabilities.SortDisplay) == 0) { - for (int i = 0; i < orderBy.Count; i++) { - if (orderBy [i].Type == OrderByType.DisplayFrom || orderBy [i].Type == OrderByType.DisplayTo) - throw new NotSupportedException ("The IMAP server does not support the SORT=DISPLAY extension."); - } - } - - var optimized = query.Optimize (new ImapSearchQueryOptimizer ()); - var expr = BuildQueryExpression (optimized, args, out charset); - var order = BuildSortOrder (orderBy); - var command = "UID SORT "; - - if ((Engine.Capabilities & ImapCapabilities.ESort) != 0) - command += "RETURN () "; - - command += order + " " + (charset ?? "US-ASCII") + " " + expr + "\r\n"; - - var ic = new ImapCommand (Engine, cancellationToken, this, command, args.ToArray ()); - if ((Engine.Capabilities & ImapCapabilities.ESort) != 0) - ic.RegisterUntaggedHandler ("ESEARCH", ESearchMatches); - else - ic.RegisterUntaggedHandler ("SORT", SearchMatches); - ic.UserData = new SearchResults (); - - Engine.QueueCommand (ic); - Engine.Wait (ic); - - ProcessResponseCodes (ic, null); + if (ic == null) + return; - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("SORT", ic); + Engine.Run (ic); - return ((SearchResults)ic.UserData).UniqueIds; + ProcessCopyToResponse (ic, destination); } /// - /// Sort messages matching the specified query. + /// Asynchronously copy the specified messages to the destination folder. /// /// - /// Searches the folder for messages matching the specified query, returning the search results in the specified sort order. + /// Copies the specified messages to the destination folder. /// - /// The search results. - /// The search options. - /// The search query. - /// The sort order. + /// An awaitable task. + /// The indexes of the messages to copy. + /// The destination folder. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// - /// is empty. - /// - /// - /// One or more search terms in the are not supported by the IMAP server. + /// One or more of the is invalid. /// -or- - /// The IMAP server does not support the ESORT extension. + /// The destination folder does not belong to the . /// /// /// The has been disposed. /// + /// + /// The is not currently open. + /// /// /// The is not connected. /// /// /// The is not authenticated. /// + /// + /// does not exist. + /// /// /// The is not currently open. /// @@ -9650,95 +5863,64 @@ static void ESearchMatches (ImapEngine engine, ImapCommand ic, int index) /// /// The server replied with a NO or BAD response. /// - public override SearchResults Sort (SearchOptions options, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)) + public override async Task CopyToAsync (IList indexes, IMailFolder destination, CancellationToken cancellationToken = default) { - var args = new List (); - string charset; - - if (query == null) - throw new ArgumentNullException (nameof (query)); - - if (orderBy == null) - throw new ArgumentNullException (nameof (orderBy)); - - if (orderBy.Count == 0) - throw new ArgumentException ("No sort order provided.", nameof (orderBy)); - - CheckState (true, false); - - if ((Engine.Capabilities & ImapCapabilities.ESort) == 0) - throw new NotSupportedException ("The IMAP server does not support the ESORT extension."); + var ic = QueueCopyToCommand (indexes, destination, cancellationToken); - if ((Engine.Capabilities & ImapCapabilities.SortDisplay) == 0) { - for (int i = 0; i < orderBy.Count; i++) { - if (orderBy[i].Type == OrderByType.DisplayFrom || orderBy[i].Type == OrderByType.DisplayTo) - throw new NotSupportedException ("The IMAP server does not support the SORT=DISPLAY extension."); - } - } + if (ic == null) + return; - var optimized = query.Optimize (new ImapSearchQueryOptimizer ()); - var expr = BuildQueryExpression (optimized, args, out charset); - var order = BuildSortOrder (orderBy); - - var command = "UID SORT RETURN ("; - if (options != SearchOptions.All && options != 0) { - if ((options & SearchOptions.All) != 0) - command += "ALL "; - if ((options & SearchOptions.Relevancy) != 0) - command += "RELEVANCY "; - if ((options & SearchOptions.Count) != 0) - command += "COUNT "; - if ((options & SearchOptions.Min) != 0) - command += "MIN "; - if ((options & SearchOptions.Max) != 0) - command += "MAX "; - command = command.TrimEnd (); - } - command += ") "; + await Engine.RunAsync (ic).ConfigureAwait (false); - command += order + " " + (charset ?? "US-ASCII") + " " + expr + "\r\n"; + ProcessCopyToResponse (ic, destination); + } - var ic = new ImapCommand (Engine, cancellationToken, this, command, args.ToArray ()); - ic.RegisterUntaggedHandler ("ESEARCH", ESearchMatches); - ic.UserData = new SearchResults (); + ImapCommand? QueueMoveToCommand (IList indexes, IMailFolder destination, CancellationToken cancellationToken) + { + ValidateArguments (indexes, destination); - Engine.QueueCommand (ic); - Engine.Wait (ic); + CheckState (true, true); + CheckAllowIndexes (); - ProcessResponseCodes (ic, null); + if (indexes.Count == 0) + return null; - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("SORT", ic); + var command = new StringBuilder ("MOVE "); + ImapUtils.FormatIndexSet (Engine, command, indexes); + command.Append (" %F\r\n"); - return (SearchResults) ic.UserData; + return Engine.QueueCommand (cancellationToken, this, command.ToString (), destination); } - static void ThreadMatches (ImapEngine engine, ImapCommand ic, int index) + void ProcessMoveToResponse (ImapCommand ic, IMailFolder destination) { - ic.UserData = ImapUtils.ParseThreads (engine, ic.Folder.UidValidity, ic.CancellationToken); + ProcessResponseCodes (ic, destination); + + ic.ThrowIfNotOk ("MOVE"); } /// - /// Threads the messages in the folder that match the search query using the specified threading algorithm. + /// Move the specified messages to the destination folder. /// /// - /// The can be used with methods such as - /// . + /// If the IMAP server supports the MOVE command, then the MOVE command will be used. Otherwise, + /// the messages will first be copied to the destination folder and then marked as \Deleted in the + /// originating folder. Since the server could disconnect at any point between those 2 operations, it + /// may be advisable to implement your own logic for moving messages in this case in order to better + /// handle spontaneous server disconnects and other error conditions. /// - /// An array of message threads. - /// The threading algorithm to use. - /// The search query. + /// The indexes of the messages to move. + /// The destination folder. /// The cancellation token. - /// - /// is not supported. - /// /// - /// is null. + /// is . + /// -or- + /// is . /// - /// - /// One or more search terms in the are not supported by the IMAP server. + /// + /// One or more of the is invalid. /// -or- - /// The server does not support the THREAD extension. + /// The destination folder does not belong to the . /// /// /// The has been disposed. @@ -9749,8 +5931,11 @@ static void ThreadMatches (ImapEngine engine, ImapCommand ic, int index) /// /// The is not authenticated. /// + /// + /// does not exist. + /// /// - /// The is not currently open. + /// The is not currently open in read-write mode. /// /// /// The operation was canceled via the cancellation token. @@ -9764,77 +5949,47 @@ static void ThreadMatches (ImapEngine engine, ImapCommand ic, int index) /// /// The server replied with a NO or BAD response. /// - public override IList Thread (ThreadingAlgorithm algorithm, SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)) + public override void MoveTo (IList indexes, IMailFolder destination, CancellationToken cancellationToken = default) { - var method = algorithm.ToString ().ToUpperInvariant (); - var args = new List (); - string charset; - - if ((Engine.Capabilities & ImapCapabilities.Thread) == 0) - throw new NotSupportedException ("The IMAP server does not support the THREAD extension."); - - if (!Engine.ThreadingAlgorithms.Contains (algorithm)) - throw new ArgumentOutOfRangeException (nameof (algorithm), "The specified threading algorithm is not supported."); - - if (query == null) - throw new ArgumentNullException (nameof (query)); - - CheckState (true, false); - - var optimized = query.Optimize (new ImapSearchQueryOptimizer ()); - var expr = BuildQueryExpression (optimized, args, out charset); - var command = "UID THREAD " + method + " " + (charset ?? "US-ASCII") + " "; - - command += expr + "\r\n"; - - var ic = new ImapCommand (Engine, cancellationToken, this, command, args.ToArray ()); - ic.RegisterUntaggedHandler ("THREAD", ThreadMatches); - - Engine.QueueCommand (ic); - Engine.Wait (ic); - - ProcessResponseCodes (ic, null); + if ((Engine.Capabilities & ImapCapabilities.Move) == 0) { + CopyTo (indexes, destination, cancellationToken); + Store (indexes, AddDeletedFlag, cancellationToken); + return; + } - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("THREAD", ic); + var ic = QueueMoveToCommand (indexes, destination, cancellationToken); - var threads = (IList) ic.UserData; + if (ic == null) + return; - if (threads == null) - return new MessageThread[0]; + Engine.Run (ic); - return threads; + ProcessMoveToResponse (ic, destination); } /// - /// Threads the messages in the folder that match the search query using the specified threading algorithm. + /// Asynchronously move the specified messages to the destination folder. /// /// - /// The can be used with methods such as - /// . + /// If the IMAP server supports the MOVE command, then the MOVE command will be used. Otherwise, + /// the messages will first be copied to the destination folder and then marked as \Deleted in the + /// originating folder. Since the server could disconnect at any point between those 2 operations, it + /// may be advisable to implement your own logic for moving messages in this case in order to better + /// handle spontaneous server disconnects and other error conditions. /// - /// An array of message threads. - /// The subset of UIDs - /// The threading algorithm to use. - /// The search query. + /// An awaitable task. + /// The indexes of the messages to move. + /// The destination folder. /// The cancellation token. - /// - /// is not supported. - /// /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// - /// is empty. - /// -or- - /// One or more of the is invalid. - /// - /// - /// One or more search terms in the are not supported by the IMAP server. + /// One or more of the is invalid. /// -or- - /// The server does not support the THREAD extension. + /// The destination folder does not belong to the . /// /// /// The has been disposed. @@ -9845,8 +6000,11 @@ static void ThreadMatches (ImapEngine engine, ImapCommand ic, int index) /// /// The is not authenticated. /// + /// + /// does not exist. + /// /// - /// The is not currently open. + /// The is not currently open in read-write mode. /// /// /// The operation was canceled via the cancellation token. @@ -9860,159 +6018,145 @@ static void ThreadMatches (ImapEngine engine, ImapCommand ic, int index) /// /// The server replied with a NO or BAD response. /// - public override IList Thread (IList uids, ThreadingAlgorithm algorithm, SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)) + public override async Task MoveToAsync (IList indexes, IMailFolder destination, CancellationToken cancellationToken = default) { - var method = algorithm.ToString ().ToUpperInvariant (); - var set = ImapUtils.FormatUidSet (uids); - var args = new List (); - string charset; - - if ((Engine.Capabilities & ImapCapabilities.Thread) == 0) - throw new NotSupportedException ("The IMAP server does not support the THREAD extension."); - - if (!Engine.ThreadingAlgorithms.Contains (algorithm)) - throw new ArgumentOutOfRangeException (nameof (algorithm), "The specified threading algorithm is not supported."); - - if (query == null) - throw new ArgumentNullException (nameof (query)); - - CheckState (true, false); - - var optimized = query.Optimize (new ImapSearchQueryOptimizer ()); - var expr = BuildQueryExpression (optimized, args, out charset); - var command = "UID THREAD " + method + " " + (charset ?? "US-ASCII") + " "; + if ((Engine.Capabilities & ImapCapabilities.Move) == 0) { + await CopyToAsync (indexes, destination, cancellationToken).ConfigureAwait (false); + await StoreAsync (indexes, AddDeletedFlag, cancellationToken).ConfigureAwait (false); + return; + } - command += "UID " + set + " " + expr + "\r\n"; + var ic = QueueMoveToCommand (indexes, destination, cancellationToken); - var ic = new ImapCommand (Engine, cancellationToken, this, command, args.ToArray ()); - ic.RegisterUntaggedHandler ("THREAD", ThreadMatches); + if (ic == null) + return; - Engine.QueueCommand (ic); - Engine.Wait (ic); + await Engine.RunAsync (ic).ConfigureAwait (false); - ProcessResponseCodes (ic, null); + ProcessMoveToResponse (ic, destination); + } - if (ic.Response != ImapCommandResponse.Ok) - throw ImapCommandException.Create ("THREAD", ic); + #region IEnumerable implementation - var threads = (IList) ic.UserData; + /// + /// Get an enumerator for the messages in the folder. + /// + /// + /// Gets an enumerator for the messages in the folder. + /// + /// The enumerator. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + public override IEnumerator GetEnumerator () + { + CheckState (true, false); - if (threads == null) - return new MessageThread[0]; + for (int i = 0; i < Count; i++) + yield return GetMessage (i, CancellationToken.None); - return threads; + yield break; } + #endregion + #region Untagged response handlers called by ImapEngine internal void OnExists (int count) { - if (Count == count) - return; - - int arrived = count - Count; - + countChanged = false; Count = count; - if (arrived > 0) - OnMessagesArrived (new MessagesArrivedEventArgs (arrived)); - OnCountChanged (); } internal void OnExpunge (int index) { + // Note: It is not required for the IMAP server to send an explicit untagged `* # EXISTS` response if it sends + // untagged `* # EXPUNGE` responses, so we queue a CountChanged event (that is only emitted if the server does + // NOT send the `* # EXISTS` response). + countChanged = true; Count--; - + OnMessageExpunged (new MessageEventArgs (index)); - OnCountChanged (); } - internal void OnFetch (ImapEngine engine, int index, CancellationToken cancellationToken) + internal void FlushQueuedEvents () { - var labelsChangedEventArgs = new MessageLabelsChangedEventArgs (index); - var flagsChangedEventArgs = new MessageFlagsChangedEventArgs (index); - var modSeqChangedEventArgs = new ModSeqChangedEventArgs (index); - var token = engine.ReadToken (cancellationToken); - bool modSeqChanged = false; - bool labelsChanged = false; - bool flagsChanged = false; - - if (token.Type != ImapTokenType.OpenParen) - throw ImapEngine.UnexpectedToken (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "FETCH", token); - - do { - token = engine.ReadToken (cancellationToken); - - if (token.Type == ImapTokenType.CloseParen || token.Type == ImapTokenType.Eoln) - break; - - if (token.Type != ImapTokenType.Atom) - throw ImapEngine.UnexpectedToken (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "FETCH", token); + if (countChanged) { + countChanged = false; + OnCountChanged (); + } + } - var atom = (string) token.Value; - ulong modseq; - uint uid; + void OnFetchAsyncCompleted (MessageSummary message) + { + int index = message.Index; + UniqueId? uid = null; - switch (atom) { - case "MODSEQ": - token = engine.ReadToken (cancellationToken); + if ((message.Fields & MessageSummaryItems.UniqueId) != 0) + uid = message.UniqueId; - if (token.Type != ImapTokenType.OpenParen) - throw ImapEngine.UnexpectedToken (ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + if (message.Flags.HasValue) { + var args = new MessageFlagsChangedEventArgs (index, message.Flags.Value, (HashSet) message.Keywords) { + ModSeq = message.ModSeq, + UniqueId = uid + }; - token = engine.ReadToken (cancellationToken); + OnMessageFlagsChanged (args); + } - if (token.Type != ImapTokenType.Atom || !ulong.TryParse ((string) token.Value, out modseq)) - throw ImapEngine.UnexpectedToken (ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + if (message.GMailLabels != null) { + var args = new MessageLabelsChangedEventArgs (index, message.GMailLabels) { + ModSeq = message.ModSeq, + UniqueId = uid + }; - token = engine.ReadToken (cancellationToken); + OnMessageLabelsChanged (args); + } - if (token.Type != ImapTokenType.CloseParen) - throw ImapEngine.UnexpectedToken (ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + if (message.Annotations != null) { + var args = new AnnotationsChangedEventArgs (index, message.Annotations) { + ModSeq = message.ModSeq, + UniqueId = uid + }; - if (modseq > HighestModSeq) - UpdateHighestModSeq (modseq); + OnAnnotationsChanged (args); + } - modSeqChangedEventArgs.ModSeq = modseq; - labelsChangedEventArgs.ModSeq = modseq; - flagsChangedEventArgs.ModSeq = modseq; - modSeqChanged = true; - break; - case "UID": - token = engine.ReadToken (cancellationToken); + if (message.ModSeq.HasValue) { + var args = new ModSeqChangedEventArgs (index, message.ModSeq.Value) { + UniqueId = uid + }; - if (token.Type != ImapTokenType.Atom || !uint.TryParse ((string) token.Value, out uid) || uid == 0) - throw ImapEngine.UnexpectedToken (ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + OnModSeqChanged (args); + } - modSeqChangedEventArgs.UniqueId = new UniqueId (UidValidity, uid); - labelsChangedEventArgs.UniqueId = new UniqueId (UidValidity, uid); - flagsChangedEventArgs.UniqueId = new UniqueId (UidValidity, uid); - break; - case "FLAGS": - flagsChangedEventArgs.Flags = ImapUtils.ParseFlagsList (engine, atom, flagsChangedEventArgs.UserFlags, cancellationToken); - flagsChanged = true; - break; - case "X-GM-LABELS": - labelsChangedEventArgs.Labels = ImapUtils.ParseLabelsList (engine, cancellationToken); - labelsChanged = true; - break; - default: - throw ImapEngine.UnexpectedToken (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "FETCH", token); - } - } while (true); + if (message.Fields != MessageSummaryItems.None) + OnMessageSummaryFetched (message); + } - if (token.Type != ImapTokenType.CloseParen) - throw ImapEngine.UnexpectedToken (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "FETCH", token); + internal void OnUntaggedFetchResponse (ImapEngine engine, int index, CancellationToken cancellationToken) + { + var message = new MessageSummary (this, index); - if (flagsChanged) - OnMessageFlagsChanged (flagsChangedEventArgs); + ParseSummaryItems (engine, message, OnFetchAsyncCompleted, cancellationToken); + } - if (labelsChanged) - OnMessageLabelsChanged (labelsChangedEventArgs); + internal Task OnUntaggedFetchResponseAsync (ImapEngine engine, int index, CancellationToken cancellationToken) + { + var message = new MessageSummary (this, index); - if (modSeqChanged) - OnModSeqChanged (modSeqChangedEventArgs); + return ParseSummaryItemsAsync (engine, message, OnFetchAsyncCompleted, cancellationToken); } internal void OnRecent (int count) @@ -10025,10 +6169,22 @@ internal void OnRecent (int count) OnRecentChanged (); } + void OnVanished (bool earlier, ImapToken token) + { + var vanished = ImapEngine.ParseUidSet (token, UidValidity, out _, out _, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "VANISHED", token); + + OnMessagesVanished (new MessagesVanishedEventArgs (vanished, earlier)); + + if (!earlier) { + Count -= vanished.Count; + + OnCountChanged (); + } + } + internal void OnVanished (ImapEngine engine, CancellationToken cancellationToken) { var token = engine.ReadToken (cancellationToken); - UniqueIdSet vanished; bool earlier = false; if (token.Type == ImapTokenType.OpenParen) { @@ -10038,37 +6194,87 @@ internal void OnVanished (ImapEngine engine, CancellationToken cancellationToken if (token.Type == ImapTokenType.CloseParen) break; - if (token.Type != ImapTokenType.Atom) - throw ImapEngine.UnexpectedToken (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "VANISHED", token); + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "VANISHED", token); var atom = (string) token.Value; - if (atom == "EARLIER") + if (atom.Equals ("EARLIER", StringComparison.OrdinalIgnoreCase)) earlier = true; } while (true); token = engine.ReadToken (cancellationToken); } - if (token.Type != ImapTokenType.Atom || !UniqueIdSet.TryParse ((string) token.Value, UidValidity, out vanished)) - throw ImapEngine.UnexpectedToken (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "VANISHED", token); + OnVanished (earlier, token); + } - OnMessagesVanished (new MessagesVanishedEventArgs (vanished, earlier)); + internal async Task OnVanishedAsync (ImapEngine engine, CancellationToken cancellationToken) + { + var token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + bool earlier = false; + + if (token.Type == ImapTokenType.OpenParen) { + do { + token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.CloseParen) + break; + + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "VANISHED", token); + + var atom = (string) token.Value; + + if (atom.Equals ("EARLIER", StringComparison.OrdinalIgnoreCase)) + earlier = true; + } while (true); + + token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + } + + OnVanished (earlier, token); } internal void UpdateAttributes (FolderAttributes attrs) { + var unsubscribed = false; + var subscribed = false; + + if ((attrs & FolderAttributes.Subscribed) == 0) + unsubscribed = (Attributes & FolderAttributes.Subscribed) != 0; + else + subscribed = (Attributes & FolderAttributes.Subscribed) == 0; + + var deleted = ((attrs & FolderAttributes.NonExistent) != 0) && + (Attributes & FolderAttributes.NonExistent) == 0; + Attributes = attrs; + + if (unsubscribed) + OnUnsubscribed (); + + if (subscribed) + OnSubscribed (); + + if (deleted) + OnDeleted (); } - internal void UpdateAcceptedFlags (MessageFlags flags) + internal void UpdateAcceptedFlags (MessageFlags flags, IReadOnlySetOfStrings keywords) { + AcceptedKeywords = keywords; AcceptedFlags = flags; } - internal void UpdatePermanentFlags (MessageFlags flags) + internal void UnsetAcceptedFlags () + { + ((HashSet) AcceptedKeywords).Clear (); + AcceptedFlags = MessageFlags.None; + } + + internal void UnsetPermanentFlags () { - PermanentFlags = flags; + ((HashSet) PermanentKeywords).Clear (); + PermanentFlags = MessageFlags.None; } internal void UpdateIsNamespace (bool value) @@ -10078,12 +6284,22 @@ internal void UpdateIsNamespace (bool value) internal void UpdateUnread (int count) { + if (Unread == count) + return; + Unread = count; + + OnUnreadChanged (); } internal void UpdateUidNext (UniqueId uid) { + if (UidNext.HasValue && UidNext.Value == uid) + return; + UidNext = uid; + + OnUidNextChanged (); } internal void UpdateAppendLimit (uint? limit) @@ -10091,6 +6307,26 @@ internal void UpdateAppendLimit (uint? limit) AppendLimit = limit; } + internal void UpdateSize (ulong? size) + { + if (Size == size) + return; + + Size = size; + + OnSizeChanged (); + } + + internal void UpdateId (string id) + { + if (Id == id) + return; + + Id = id; + + OnIdChanged (); + } + internal void UpdateHighestModSeq (ulong modseq) { if (HighestModSeq == modseq) @@ -10111,41 +6347,21 @@ internal void UpdateUidValidity (uint validity) OnUidValidityChanged (); } - #endregion - - #endregion - - #region IEnumerable implementation - - /// - /// Gets an enumerator for the messages in the folder. - /// - /// - /// Gets an enumerator for the messages in the folder. - /// - /// The enumerator. - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The is not currently open. - /// - public override IEnumerator GetEnumerator () + internal void OnRenamed (string encodedName, char delim, FolderAttributes attrs) { - CheckState (true, false); + var oldFullName = FullName; - for (int i = 0; i < Count; i++) - yield return GetMessage (i, CancellationToken.None); + EncodedName = encodedName; + FullName = Engine.DecodeMailboxName (encodedName); + Name = GetBaseName (FullName, delim); + DirectorySeparator = delim; + Attributes = attrs; - yield break; + OnRenamed (oldFullName, FullName); } #endregion + + #endregion } } diff --git a/MailKit/Net/Imap/ImapFolderAnnotations.cs b/MailKit/Net/Imap/ImapFolderAnnotations.cs new file mode 100644 index 0000000000..3fa7a10b4f --- /dev/null +++ b/MailKit/Net/Imap/ImapFolderAnnotations.cs @@ -0,0 +1,620 @@ +// +// ImapFolderAnnotations.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Text; +using System.Threading; +using System.Globalization; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace MailKit.Net.Imap +{ + public partial class ImapFolder + { + IEnumerable QueueStoreCommands (IList uids, ulong? modseq, IList annotations, CancellationToken cancellationToken) + { + if (uids == null) + throw new ArgumentNullException (nameof (uids)); + + if (modseq.HasValue && !supportsModSeq) + throw new NotSupportedException ("The ImapFolder does not support mod-sequences."); + + if (annotations == null) + throw new ArgumentNullException (nameof (annotations)); + + CheckState (true, true); + + if (AnnotationAccess == AnnotationAccess.None) + throw new NotSupportedException ("The ImapFolder does not support annotations."); + + if (uids.Count == 0 || annotations.Count == 0) + return Array.Empty (); + + var builder = new StringBuilder ("UID STORE %s "); + var values = new List (); + + if (modseq.HasValue) { + builder.Append ("(UNCHANGEDSINCE "); + builder.Append (modseq.Value.ToString (CultureInfo.InvariantCulture)); + builder.Append (") "); + } + + ImapUtils.FormatAnnotations (builder, annotations, values, true); + builder.Append ("\r\n"); + + var command = builder.ToString (); + var args = values.ToArray (); + + return Engine.QueueCommands (cancellationToken, this, command, uids, args); + } + + void ProcessStoreAnnotationsResponse (ImapCommand ic) + { + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) { + // TODO: Do something with the AnnotateResponseCode if it exists?? + + throw ImapCommandException.Create ("STORE", ic); + } + } + + /// + /// Store the annotations for the specified messages. + /// + /// + /// Stores the annotations for the specified messages. + /// + /// The UIDs of the messages. + /// The annotations to store. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// Cannot store annotations without any properties defined. + /// + /// + /// The does not support annotations. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override void Store (IList uids, IList annotations, CancellationToken cancellationToken = default) + { + foreach (var ic in QueueStoreCommands (uids, null, annotations, cancellationToken)) { + Engine.Run (ic); + + ProcessStoreAnnotationsResponse (ic); + } + } + + /// + /// Asynchronously store the annotations for the specified messages. + /// + /// + /// Asynchronously stores the annotations for the specified messages. + /// + /// An asynchronous task context. + /// The UIDs of the messages. + /// The annotations to store. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// Cannot store annotations without any properties defined. + /// + /// + /// The does not support annotations. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override async Task StoreAsync (IList uids, IList annotations, CancellationToken cancellationToken = default) + { + foreach (var ic in QueueStoreCommands (uids, null, annotations, cancellationToken)) { + await Engine.RunAsync (ic).ConfigureAwait (false); + + ProcessStoreAnnotationsResponse (ic); + } + } + + /// + /// Store the annotations for the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Stores the annotations for the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The annotations to store. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// Cannot store annotations without any properties defined. + /// + /// + /// The does not support annotations. + /// -or- + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override IList Store (IList uids, ulong modseq, IList annotations, CancellationToken cancellationToken = default) + { + UniqueIdSet? unmodified = null; + + foreach (var ic in QueueStoreCommands (uids, modseq, annotations, cancellationToken)) { + Engine.Run (ic); + + ProcessStoreAnnotationsResponse (ic); + + ProcessUnmodified (ic, ref unmodified, modseq); + } + + if (unmodified == null) + return Array.Empty (); + + return unmodified; + } + + /// + /// Asynchronously store the annotations for the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously stores the annotations for the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The annotations to store. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// Cannot store annotations without any properties defined. + /// + /// + /// The does not support annotations. + /// -or- + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override async Task> StoreAsync (IList uids, ulong modseq, IList annotations, CancellationToken cancellationToken = default) + { + UniqueIdSet? unmodified = null; + + foreach (var ic in QueueStoreCommands (uids, modseq, annotations, cancellationToken)) { + await Engine.RunAsync (ic).ConfigureAwait (false); + + ProcessStoreAnnotationsResponse (ic); + + ProcessUnmodified (ic, ref unmodified, modseq); + } + + if (unmodified == null) + return Array.Empty (); + + return unmodified; + } + + bool TryQueueStoreCommand (IList indexes, ulong? modseq, IList annotations, CancellationToken cancellationToken, [NotNullWhen (true)] out ImapCommand? ic) + { + if (indexes == null) + throw new ArgumentNullException (nameof (indexes)); + + if (modseq.HasValue && !supportsModSeq) + throw new NotSupportedException ("The ImapFolder does not support mod-sequences."); + + if (annotations == null) + throw new ArgumentNullException (nameof (annotations)); + + CheckState (true, true); + + if (AnnotationAccess == AnnotationAccess.None) + throw new NotSupportedException ("The ImapFolder does not support annotations."); + + if (indexes.Count == 0 || annotations.Count == 0) { + ic = null; + return false; + } + + var command = new StringBuilder ("STORE "); + var args = new List (); + + ImapUtils.FormatIndexSet (Engine, command, indexes); + command.Append (' '); + + if (modseq.HasValue) { + command.Append ("(UNCHANGEDSINCE "); + command.Append (modseq.Value.ToString (CultureInfo.InvariantCulture)); + command.Append (") "); + } + + ImapUtils.FormatAnnotations (command, annotations, args, true); + command.Append ("\r\n"); + + ic = Engine.QueueCommand (cancellationToken, this, command.ToString (), args.ToArray ()); + + return true; + } + + /// + /// Store the annotations for the specified messages. + /// + /// + /// Stores the annotations for the specified messages. + /// + /// The indexes of the messages. + /// The annotations to store. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// Cannot store annotations without any properties defined. + /// + /// + /// The does not support annotations. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override void Store (IList indexes, IList annotations, CancellationToken cancellationToken = default) + { + if (!TryQueueStoreCommand (indexes, null, annotations, cancellationToken, out var ic)) + return; + + Engine.Run (ic); + + ProcessStoreAnnotationsResponse (ic); + } + + /// + /// Asynchronously store the annotations for the specified messages. + /// + /// + /// Asynchronously stores the annotations for the specified messages. + /// + /// An asynchronous task context. + /// The indexes of the messages. + /// The annotations to store. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// Cannot store annotations without any properties defined. + /// + /// + /// The does not support annotations. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override async Task StoreAsync (IList indexes, IList annotations, CancellationToken cancellationToken = default) + { + if (!TryQueueStoreCommand (indexes, null, annotations, cancellationToken, out var ic)) + return; + + await Engine.RunAsync (ic).ConfigureAwait (false); + + ProcessStoreAnnotationsResponse (ic); + } + + /// + /// Store the annotations for the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Stores the annotations for the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The annotations to store. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// Cannot store annotations without any properties defined. + /// + /// + /// The does not support annotations. + /// -or- + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override IList Store (IList indexes, ulong modseq, IList annotations, CancellationToken cancellationToken = default) + { + if (!TryQueueStoreCommand (indexes, modseq, annotations, cancellationToken, out var ic)) + return Array.Empty (); + + Engine.Run (ic); + + ProcessStoreAnnotationsResponse (ic); + + return GetUnmodified (ic, modseq); + } + + /// + /// Asynchronously store the annotations for the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously stores the annotations for the specified messages only if their mod-sequence value is less than the specified value.s + /// + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The annotations to store. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// Cannot store annotations without any properties defined. + /// + /// + /// The does not support annotations. + /// -or- + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override async Task> StoreAsync (IList indexes, ulong modseq, IList annotations, CancellationToken cancellationToken = default) + { + if (!TryQueueStoreCommand (indexes, modseq, annotations, cancellationToken, out var ic)) + return Array.Empty (); + + await Engine.RunAsync (ic).ConfigureAwait (false); + + ProcessStoreAnnotationsResponse (ic); + + return GetUnmodified (ic, modseq); + } + } +} diff --git a/MailKit/Net/Imap/ImapFolderConstructorArgs.cs b/MailKit/Net/Imap/ImapFolderConstructorArgs.cs index a953247c8a..5548e5e035 100644 --- a/MailKit/Net/Imap/ImapFolderConstructorArgs.cs +++ b/MailKit/Net/Imap/ImapFolderConstructorArgs.cs @@ -1,9 +1,9 @@ -// +// // ImapFolderInfo.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -31,7 +31,9 @@ namespace MailKit.Net.Imap { /// Constructor arguments for . /// /// - /// Constructor arguments for . + /// Constructor arguments for . + /// The are meant only to allow subclassing of + /// by overriding the method. /// public sealed class ImapFolderConstructorArgs { @@ -48,17 +50,12 @@ public sealed class ImapFolderConstructorArgs internal ImapFolderConstructorArgs (ImapEngine engine, string encodedName, FolderAttributes attributes, char delim) { FullName = engine.DecodeMailboxName (encodedName); - Name = GetBaseName (FullName, delim); DirectorySeparator = delim; EncodedName = encodedName; Attributes = attributes; Engine = engine; } - ImapFolderConstructorArgs () - { - } - /// /// Get the folder attributes. /// @@ -99,15 +96,9 @@ public string FullName { /// This is the equivalent of the file name of a file on the file system. /// /// The name of the folder. + [Obsolete] public string Name { - get; private set; - } - - static string GetBaseName (string fullName, char delim) - { - var names = fullName.Split (new [] { delim }, StringSplitOptions.RemoveEmptyEntries); - - return names.Length > 0 ? names[names.Length - 1] : fullName; + get { return MailFolder.GetBaseName (FullName, DirectorySeparator); } } } } diff --git a/MailKit/Net/Imap/ImapFolderFetch.cs b/MailKit/Net/Imap/ImapFolderFetch.cs new file mode 100644 index 0000000000..6b7b9c5583 --- /dev/null +++ b/MailKit/Net/Imap/ImapFolderFetch.cs @@ -0,0 +1,5596 @@ +// +// ImapFolderFetch.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Text; +using System.Buffers; +using System.Threading; +using System.Globalization; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +using MimeKit; +using MimeKit.IO; +using MimeKit.Text; +using MimeKit.Utils; + +using MailKit.Search; + +namespace MailKit.Net.Imap +{ + public partial class ImapFolder + { + const int PreviewHtmlLength = 16 * 1024; + const int PreviewTextLength = 512; + const int BufferSize = 4096; + + class FetchSummaryContext + { + public readonly List Messages; + + public FetchSummaryContext (int capacity) + { + Messages = new List (capacity); + } + + int BinarySearch (int index, bool insert) + { + int min = 0, max = Messages.Count; + + if (max == 0) + return insert ? 0 : -1; + + if (insert && index > Messages[max - 1].Index) + return max; + + do { + int i = min + ((max - min) / 2); + + if (index == Messages[i].Index) + return i; + + if (index > Messages[i].Index) { + min = i + 1; + } else { + max = i; + } + } while (min < max); + + return insert ? min : -1; + } + + public void Add (int index, MessageSummary message) + { + int i = BinarySearch (index, true); + + if (i < Messages.Count) + Messages.Insert (i, message); + else + Messages.Add (message); + } + + public bool TryGetValue (int index, [NotNullWhen (true)] out MessageSummary? message) + { + int i; + + if ((i = BinarySearch (index, false)) == -1) { + message = null; + return false; + } + + message = (MessageSummary) Messages[i]; + + return true; + } + + public void OnMessageExpunged (object? sender, MessageEventArgs args) + { + int index = BinarySearch (args.Index, false); + + if (index == -1) + return; + + Messages.RemoveAt (index); + + for (int i = index; i < Messages.Count; i++) { + var message = (MessageSummary) Messages[i]; + message.Index--; + } + } + } + + static void ReadLiteralData (ImapEngine engine, CancellationToken cancellationToken) + { + var buf = ArrayPool.Shared.Rent (BufferSize); + int nread; + + try { + do { + nread = engine.Stream!.Read (buf, 0, BufferSize, cancellationToken); + } while (nread > 0); + } finally { + ArrayPool.Shared.Return (buf); + } + } + + static async Task ReadLiteralDataAsync (ImapEngine engine, CancellationToken cancellationToken) + { + var buf = ArrayPool.Shared.Rent (BufferSize); + int nread; + + try { + do { + nread = await engine.Stream!.ReadAsync (buf, 0, BufferSize, cancellationToken).ConfigureAwait (false); + } while (nread > 0); + } finally { + ArrayPool.Shared.Return (buf); + } + } + + static void SkipParenthesizedList (ImapEngine engine, CancellationToken cancellationToken) + { + int depth = 1; + + do { + var token = engine.PeekToken (cancellationToken); + + if (token.Type == ImapTokenType.Eoln) + return; + + // token is safe to read, so pop it off the queue + token = engine.ReadToken (cancellationToken); + + if (token.Type == ImapTokenType.CloseParen) { + depth--; + } else if (token.Type == ImapTokenType.OpenParen) { + depth++; + } else if (token.Type == ImapTokenType.Literal) { + // consume the literal string + ReadLiteralData (engine, cancellationToken); + } + } while (depth > 0); + } + + static async Task SkipParenthesizedListAsync (ImapEngine engine, CancellationToken cancellationToken) + { + int depth = 1; + + do { + var token = await engine.PeekTokenAsync (cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.Eoln) + return; + + // token is safe to read, so pop it off the queue + token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.CloseParen) { + depth--; + } else if (token.Type == ImapTokenType.OpenParen) { + depth++; + } else if (token.Type == ImapTokenType.Literal) { + // consume the literal string + await ReadLiteralDataAsync (engine, cancellationToken).ConfigureAwait (false); + } + } while (depth > 0); + } + + static DateTimeOffset? ReadDateTimeOffsetToken (ImapEngine engine, string atom, CancellationToken cancellationToken) + { + var token = engine.ReadToken (cancellationToken); + + switch (token.Type) { + case ImapTokenType.QString: + case ImapTokenType.Atom: + return ImapUtils.ParseInternalDate ((string) token.Value); + case ImapTokenType.Nil: + return null; + default: + throw ImapEngine.UnexpectedToken (ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + } + } + + static async Task ReadDateTimeOffsetTokenAsync (ImapEngine engine, string atom, CancellationToken cancellationToken) + { + var token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + switch (token.Type) { + case ImapTokenType.QString: + case ImapTokenType.Atom: + return ImapUtils.ParseInternalDate ((string) token.Value); + case ImapTokenType.Nil: + return null; + default: + throw ImapEngine.UnexpectedToken (ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + } + } + + delegate void FetchSummaryItemsCompletedCallback (MessageSummary message); + + void ParseSummaryItems (ImapEngine engine, MessageSummary message, FetchSummaryItemsCompletedCallback completed, CancellationToken cancellationToken) + { + var token = engine.ReadToken (cancellationToken); + + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "FETCH", token); + + do { + token = engine.ReadToken (cancellationToken); + + if (token.Type == ImapTokenType.CloseParen || token.Type == ImapTokenType.Eoln) + break; + + bool parenthesized = false; + if (engine.QuirksMode == ImapQuirksMode.Domino && token.Type == ImapTokenType.OpenParen) { + // Note: Lotus Domino IMAP will (sometimes?) encapsulate the `ENVELOPE` segment of the + // response within an extra set of parenthesis. + // + // See https://github.com/jstedfast/MailKit/issues/943 for details. + token = engine.ReadToken (cancellationToken); + parenthesized = true; + } + + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "FETCH", token); + + var atom = (string) token.Value; + string format; + ulong value64; + uint value; + int idx; + + if (atom.Equals ("INTERNALDATE", StringComparison.OrdinalIgnoreCase)) { + message.InternalDate = ReadDateTimeOffsetToken (engine, atom, cancellationToken); + message.Fields |= MessageSummaryItems.InternalDate; + } else if (atom.Equals ("SAVEDATE", StringComparison.OrdinalIgnoreCase)) { + message.SaveDate = ReadDateTimeOffsetToken (engine, atom, cancellationToken); + message.Fields |= MessageSummaryItems.SaveDate; + } else if (atom.Equals ("RFC822.SIZE", StringComparison.OrdinalIgnoreCase)) { + token = engine.ReadToken (cancellationToken); + + message.Size = ImapEngine.ParseNumber (token, false, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + message.Fields |= MessageSummaryItems.Size; + } else if (atom.Equals ("BODYSTRUCTURE", StringComparison.OrdinalIgnoreCase)) { + format = string.Format (ImapEngine.GenericItemSyntaxErrorFormat, "BODYSTRUCTURE", "{0}"); + message.Body = ImapUtils.ParseBody (engine, format, string.Empty, cancellationToken); + message.Fields |= MessageSummaryItems.BodyStructure; + } else if (atom.Equals ("BODY", StringComparison.OrdinalIgnoreCase)) { + token = engine.PeekToken (cancellationToken); + format = ImapEngine.FetchBodySyntaxErrorFormat; + + if (token.Type == ImapTokenType.OpenBracket) { + var referencesField = false; + var headerFields = false; + + // consume the '[' + token = engine.ReadToken (cancellationToken); + + ImapEngine.AssertToken (token, ImapTokenType.OpenBracket, format, token); + + // References and/or other headers were requested... + + do { + token = engine.ReadToken (cancellationToken); + + if (token.Type == ImapTokenType.CloseBracket) + break; + + if (token.Type == ImapTokenType.OpenParen) { + do { + token = engine.ReadToken (cancellationToken); + + if (token.Type == ImapTokenType.CloseParen) + break; + + // the header field names will generally be atoms or qstrings but may also be literals + engine.UngetToken (token); + + var field = ImapUtils.ReadStringToken (engine, format, cancellationToken); + + if (headerFields && !referencesField && field.Equals ("REFERENCES", StringComparison.OrdinalIgnoreCase)) + referencesField = true; + } while (true); + } else { + ImapEngine.AssertToken (token, ImapTokenType.Atom, format, token); + + atom = (string) token.Value; + + if (atom.Equals ("HEADER", StringComparison.OrdinalIgnoreCase)) { + // if we're fetching *all* headers, then it will include the References header (if it exists) + referencesField = true; + headerFields = false; + } else { + headerFields = atom.Equals ("HEADER.FIELDS", StringComparison.OrdinalIgnoreCase); + } + } + } while (true); + + ImapEngine.AssertToken (token, ImapTokenType.CloseBracket, format, token); + + token = engine.ReadToken (cancellationToken); + + ImapEngine.AssertToken (token, ImapTokenType.Literal, format, token); + + try { + message.Headers = engine.ParseHeaders (engine.Stream!, cancellationToken); + } catch (FormatException) { + message.Headers = new HeaderList (); + } + + // consume any remaining literal data... (typically extra blank lines) + ReadLiteralData (engine, cancellationToken); + + message.References = new MessageIdList (); + + if ((idx = message.Headers.IndexOf (HeaderId.References)) != -1) { + var references = message.Headers[idx]; + var rawValue = references.RawValue; + + foreach (var msgid in MimeUtils.EnumerateReferences (rawValue, 0, rawValue.Length)) + message.References.Add (msgid); + } + + message.Fields |= MessageSummaryItems.Headers; + + if (referencesField) + message.Fields |= MessageSummaryItems.References; + } else { + message.Body = ImapUtils.ParseBody (engine, format, string.Empty, cancellationToken); + message.Fields |= MessageSummaryItems.Body; + } + } else if (atom.Equals ("ENVELOPE", StringComparison.OrdinalIgnoreCase)) { + message.Envelope = ImapUtils.ParseEnvelope (engine, cancellationToken); + message.Fields |= MessageSummaryItems.Envelope; + } else if (atom.Equals ("FLAGS", StringComparison.OrdinalIgnoreCase)) { + message.Flags = ImapUtils.ParseFlagsList (engine, atom, (HashSet) message.Keywords, cancellationToken); + message.Fields |= MessageSummaryItems.Flags; + } else if (atom.Equals ("MODSEQ", StringComparison.OrdinalIgnoreCase)) { + token = engine.ReadToken (cancellationToken); + + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + + token = engine.ReadToken (cancellationToken); + + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + + // Note: Some IMAP servers (such as Zoho Mail) will return a MODSEQ of -1 in some cases (not sure why). + // + // If we get an invalid value, just ignore it. + // + // See https://github.com/jstedfast/MailKit/issues/1686 for details. + if (ImapEngine.TryParseNumber64 (token, out value64)) { + message.Fields |= MessageSummaryItems.ModSeq; + message.ModSeq = value64; + + if (value64 > HighestModSeq) + UpdateHighestModSeq (value64); + } + + token = engine.ReadToken (cancellationToken); + + ImapEngine.AssertToken (token, ImapTokenType.CloseParen, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + } else if (atom.Equals ("UID", StringComparison.OrdinalIgnoreCase)) { + token = engine.ReadToken (cancellationToken); + + value = ImapEngine.ParseNumber (token, true, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + + message.UniqueId = new UniqueId (UidValidity, value); + message.Fields |= MessageSummaryItems.UniqueId; + } else if (atom.Equals ("EMAILID", StringComparison.OrdinalIgnoreCase)) { + token = engine.ReadToken (cancellationToken); + + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + + token = engine.ReadToken (cancellationToken); + + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + + message.Fields |= MessageSummaryItems.EmailId; + message.EmailId = (string) token.Value; + + token = engine.ReadToken (cancellationToken); + + ImapEngine.AssertToken (token, ImapTokenType.CloseParen, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + } else if (atom.Equals ("THREADID", StringComparison.OrdinalIgnoreCase)) { + token = engine.ReadToken (cancellationToken); + + if (token.Type == ImapTokenType.OpenParen) { + token = engine.ReadToken (cancellationToken); + + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + + message.Fields |= MessageSummaryItems.ThreadId; + message.ThreadId = (string) token.Value; + + token = engine.ReadToken (cancellationToken); + + ImapEngine.AssertToken (token, ImapTokenType.CloseParen, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + } else { + ImapEngine.AssertToken (token, ImapTokenType.Nil, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + + message.Fields |= MessageSummaryItems.ThreadId; + message.ThreadId = null; + } + } else if (atom.Equals ("X-GM-MSGID", StringComparison.OrdinalIgnoreCase)) { + token = engine.ReadToken (cancellationToken); + + value64 = ImapEngine.ParseNumber64 (token, true, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + message.Fields |= MessageSummaryItems.GMailMessageId; + message.GMailMessageId = value64; + } else if (atom.Equals ("X-GM-THRID", StringComparison.OrdinalIgnoreCase)) { + token = engine.ReadToken (cancellationToken); + + value64 = ImapEngine.ParseNumber64 (token, true, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + message.Fields |= MessageSummaryItems.GMailThreadId; + message.GMailThreadId = value64; + } else if (atom.Equals ("X-GM-LABELS", StringComparison.OrdinalIgnoreCase)) { + message.GMailLabels = ImapUtils.ParseLabelsList (engine, cancellationToken); + message.Fields |= MessageSummaryItems.GMailLabels; + } else if (atom.Equals ("ANNOTATION", StringComparison.OrdinalIgnoreCase)) { + message.Annotations = ImapUtils.ParseAnnotations (engine, cancellationToken); + message.Fields |= MessageSummaryItems.Annotations; + } else if (atom.Equals ("PREVIEW", StringComparison.OrdinalIgnoreCase)) { + format = string.Format (ImapEngine.GenericItemSyntaxErrorFormat, "PREVIEW", "{0}"); + message.PreviewText = ImapUtils.ReadNStringToken (engine, format, false, cancellationToken); + message.Fields |= MessageSummaryItems.PreviewText; + } else { + // Unexpected or unknown token (such as XAOL.SPAM.REASON or XAOL-MSGID). Simply read 1 more token (the argument) and ignore. + token = engine.ReadToken (cancellationToken); + + if (token.Type == ImapTokenType.OpenParen) + SkipParenthesizedList (engine, cancellationToken); + } + + if (parenthesized) { + // Note: This is the second half of the Lotus Domino IMAP server work-around. + token = engine.ReadToken (cancellationToken); + ImapEngine.AssertToken (token, ImapTokenType.CloseParen, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "FETCH", token); + } + } while (true); + + ImapEngine.AssertToken (token, ImapTokenType.CloseParen, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "FETCH", token); + + completed (message); + } + + async Task ParseSummaryItemsAsync (ImapEngine engine, MessageSummary message, FetchSummaryItemsCompletedCallback completed, CancellationToken cancellationToken) + { + var token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "FETCH", token); + + do { + token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.CloseParen || token.Type == ImapTokenType.Eoln) + break; + + bool parenthesized = false; + if (engine.QuirksMode == ImapQuirksMode.Domino && token.Type == ImapTokenType.OpenParen) { + // Note: Lotus Domino IMAP will (sometimes?) encapsulate the `ENVELOPE` segment of the + // response within an extra set of parenthesis. + // + // See https://github.com/jstedfast/MailKit/issues/943 for details. + token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + parenthesized = true; + } + + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "FETCH", token); + + var atom = (string) token.Value; + string format; + ulong value64; + uint value; + int idx; + + if (atom.Equals ("INTERNALDATE", StringComparison.OrdinalIgnoreCase)) { + message.InternalDate = await ReadDateTimeOffsetTokenAsync (engine, atom, cancellationToken).ConfigureAwait (false); + message.Fields |= MessageSummaryItems.InternalDate; + } else if (atom.Equals ("SAVEDATE", StringComparison.OrdinalIgnoreCase)) { + message.SaveDate = await ReadDateTimeOffsetTokenAsync (engine, atom, cancellationToken).ConfigureAwait (false); + message.Fields |= MessageSummaryItems.SaveDate; + } else if (atom.Equals ("RFC822.SIZE", StringComparison.OrdinalIgnoreCase)) { + token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + message.Size = ImapEngine.ParseNumber (token, false, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + message.Fields |= MessageSummaryItems.Size; + } else if (atom.Equals ("BODYSTRUCTURE", StringComparison.OrdinalIgnoreCase)) { + format = string.Format (ImapEngine.GenericItemSyntaxErrorFormat, "BODYSTRUCTURE", "{0}"); + message.Body = await ImapUtils.ParseBodyAsync (engine, format, string.Empty, cancellationToken).ConfigureAwait (false); + message.Fields |= MessageSummaryItems.BodyStructure; + } else if (atom.Equals ("BODY", StringComparison.OrdinalIgnoreCase)) { + token = await engine.PeekTokenAsync (cancellationToken).ConfigureAwait (false); + format = ImapEngine.FetchBodySyntaxErrorFormat; + + if (token.Type == ImapTokenType.OpenBracket) { + var referencesField = false; + var headerFields = false; + + // consume the '[' + token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + ImapEngine.AssertToken (token, ImapTokenType.OpenBracket, format, token); + + // References and/or other headers were requested... + + do { + token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.CloseBracket) + break; + + if (token.Type == ImapTokenType.OpenParen) { + do { + token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.CloseParen) + break; + + // the header field names will generally be atoms or qstrings but may also be literals + engine.UngetToken (token); + + var field = await ImapUtils.ReadStringTokenAsync (engine, format, cancellationToken).ConfigureAwait (false); + + if (headerFields && !referencesField && field.Equals ("REFERENCES", StringComparison.OrdinalIgnoreCase)) + referencesField = true; + } while (true); + } else { + ImapEngine.AssertToken (token, ImapTokenType.Atom, format, token); + + atom = (string) token.Value; + + if (atom.Equals ("HEADER", StringComparison.OrdinalIgnoreCase)) { + // if we're fetching *all* headers, then it will include the References header (if it exists) + referencesField = true; + headerFields = false; + } else { + headerFields = atom.Equals ("HEADER.FIELDS", StringComparison.OrdinalIgnoreCase); + } + } + } while (true); + + ImapEngine.AssertToken (token, ImapTokenType.CloseBracket, format, token); + + token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + ImapEngine.AssertToken (token, ImapTokenType.Literal, format, token); + + try { + message.Headers = await engine.ParseHeadersAsync (engine.Stream!, cancellationToken).ConfigureAwait (false); + } catch (FormatException) { + message.Headers = new HeaderList (); + } + + // consume any remaining literal data... (typically extra blank lines) + await ReadLiteralDataAsync (engine, cancellationToken).ConfigureAwait (false); + + message.References = new MessageIdList (); + + if ((idx = message.Headers.IndexOf (HeaderId.References)) != -1) { + var references = message.Headers[idx]; + var rawValue = references.RawValue; + + foreach (var msgid in MimeUtils.EnumerateReferences (rawValue, 0, rawValue.Length)) + message.References.Add (msgid); + } + + message.Fields |= MessageSummaryItems.Headers; + + if (referencesField) + message.Fields |= MessageSummaryItems.References; + } else { + message.Body = await ImapUtils.ParseBodyAsync (engine, format, string.Empty, cancellationToken).ConfigureAwait (false); + message.Fields |= MessageSummaryItems.Body; + } + } else if (atom.Equals ("ENVELOPE", StringComparison.OrdinalIgnoreCase)) { + message.Envelope = await ImapUtils.ParseEnvelopeAsync (engine, cancellationToken).ConfigureAwait (false); + message.Fields |= MessageSummaryItems.Envelope; + } else if (atom.Equals ("FLAGS", StringComparison.OrdinalIgnoreCase)) { + message.Flags = await ImapUtils.ParseFlagsListAsync (engine, atom, (HashSet) message.Keywords, cancellationToken).ConfigureAwait (false); + message.Fields |= MessageSummaryItems.Flags; + } else if (atom.Equals ("MODSEQ", StringComparison.OrdinalIgnoreCase)) { + token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + + token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + + // Note: Some IMAP servers (such as Zoho Mail) will return a MODSEQ of -1 in some cases (not sure why). + // + // If we get an invalid value, just ignore it. + // + // See https://github.com/jstedfast/MailKit/issues/1686 for details. + if (ImapEngine.TryParseNumber64 (token, out value64)) { + message.Fields |= MessageSummaryItems.ModSeq; + message.ModSeq = value64; + + if (value64 > HighestModSeq) + UpdateHighestModSeq (value64); + } + + token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + ImapEngine.AssertToken (token, ImapTokenType.CloseParen, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + } else if (atom.Equals ("UID", StringComparison.OrdinalIgnoreCase)) { + token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + value = ImapEngine.ParseNumber (token, true, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + + message.UniqueId = new UniqueId (UidValidity, value); + message.Fields |= MessageSummaryItems.UniqueId; + } else if (atom.Equals ("EMAILID", StringComparison.OrdinalIgnoreCase)) { + token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + + token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + + message.Fields |= MessageSummaryItems.EmailId; + message.EmailId = (string) token.Value; + + token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + ImapEngine.AssertToken (token, ImapTokenType.CloseParen, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + } else if (atom.Equals ("THREADID", StringComparison.OrdinalIgnoreCase)) { + token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.OpenParen) { + token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + + message.Fields |= MessageSummaryItems.ThreadId; + message.ThreadId = (string) token.Value; + + token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + ImapEngine.AssertToken (token, ImapTokenType.CloseParen, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + } else { + ImapEngine.AssertToken (token, ImapTokenType.Nil, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + + message.Fields |= MessageSummaryItems.ThreadId; + message.ThreadId = null; + } + } else if (atom.Equals ("X-GM-MSGID", StringComparison.OrdinalIgnoreCase)) { + token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + value64 = ImapEngine.ParseNumber64 (token, true, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + message.Fields |= MessageSummaryItems.GMailMessageId; + message.GMailMessageId = value64; + } else if (atom.Equals ("X-GM-THRID", StringComparison.OrdinalIgnoreCase)) { + token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + value64 = ImapEngine.ParseNumber64 (token, true, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + message.Fields |= MessageSummaryItems.GMailThreadId; + message.GMailThreadId = value64; + } else if (atom.Equals ("X-GM-LABELS", StringComparison.OrdinalIgnoreCase)) { + message.GMailLabels = await ImapUtils.ParseLabelsListAsync (engine, cancellationToken).ConfigureAwait (false); + message.Fields |= MessageSummaryItems.GMailLabels; + } else if (atom.Equals ("ANNOTATION", StringComparison.OrdinalIgnoreCase)) { + message.Annotations = await ImapUtils.ParseAnnotationsAsync (engine, cancellationToken).ConfigureAwait (false); + message.Fields |= MessageSummaryItems.Annotations; + } else if (atom.Equals ("PREVIEW", StringComparison.OrdinalIgnoreCase)) { + format = string.Format (ImapEngine.GenericItemSyntaxErrorFormat, "PREVIEW", "{0}"); + message.PreviewText = await ImapUtils.ReadNStringTokenAsync (engine, format, false, cancellationToken).ConfigureAwait (false); + message.Fields |= MessageSummaryItems.PreviewText; + } else { + // Unexpected or unknown token (such as XAOL.SPAM.REASON or XAOL-MSGID). Simply read 1 more token (the argument) and ignore. + token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.OpenParen) + await SkipParenthesizedListAsync (engine, cancellationToken).ConfigureAwait (false); + } + + if (parenthesized) { + // Note: This is the second half of the Lotus Domino IMAP server work-around. + token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + ImapEngine.AssertToken (token, ImapTokenType.CloseParen, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "FETCH", token); + } + } while (true); + + ImapEngine.AssertToken (token, ImapTokenType.CloseParen, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "FETCH", token); + + completed (message); + } + + Task UntaggedFetchSummaryItemsHandler (ImapEngine engine, ImapCommand ic, int index, bool doAsync) + { + var ctx = (FetchSummaryContext) ic.UserData!; + + if (!ctx.TryGetValue (index, out var message)) { + message = new MessageSummary (this, index); + ctx.Add (index, message); + } + + if (doAsync) + return ParseSummaryItemsAsync (engine, message, OnMessageSummaryFetched, ic.CancellationToken); + + ParseSummaryItems (engine, message, OnMessageSummaryFetched, ic.CancellationToken); + + return Task.CompletedTask; + } + + static bool IsEmptyExclude (HeaderSet headers, bool requestReferences) + { + // return whether or not we've got an empty set of excluded headers or if the user has requested the References header *and* asked us to exclude only the References header. + return headers.Exclude && (headers.Count == 0 || (headers.Count == 1 && requestReferences && headers.Contains ("REFERENCES"))); + } + + internal static string FormatSummaryItems (ImapEngine engine, IFetchRequest request, out bool previewText, bool isNotify = false) + { + var items = request.Items; + + if ((engine.Capabilities & ImapCapabilities.Preview) == 0 && (items & MessageSummaryItems.PreviewText) != 0) { + // if the user wants the preview text, we will also need the UIDs and BODYSTRUCTUREs + // so that we can request a preview of the body text in subsequent FETCH requests. + items |= MessageSummaryItems.BodyStructure | MessageSummaryItems.UniqueId; + items &= ~MessageSummaryItems.PreviewText; + previewText = true; + } else { + previewText = false; + } + + if ((items & MessageSummaryItems.BodyStructure) != 0 && (items & MessageSummaryItems.Body) != 0) { + // don't query both the BODY and BODYSTRUCTURE, that's just dumb... + items &= ~MessageSummaryItems.Body; + } + + if (engine.QuirksMode != ImapQuirksMode.GMail && !isNotify) { + if (items == MessageSummaryItems.All) + return "ALL"; + + if (items == MessageSummaryItems.Full) + return "FULL"; + + if (items == MessageSummaryItems.Fast) + return "FAST"; + } + + var tokens = new List (); + + // now add on any additional summary items... + if ((items & MessageSummaryItems.UniqueId) != 0) + tokens.Add ("UID"); + if ((items & MessageSummaryItems.Flags) != 0) + tokens.Add ("FLAGS"); + if ((items & MessageSummaryItems.InternalDate) != 0) + tokens.Add ("INTERNALDATE"); + if ((items & MessageSummaryItems.Size) != 0) + tokens.Add ("RFC822.SIZE"); + if ((items & MessageSummaryItems.Envelope) != 0) + tokens.Add ("ENVELOPE"); + if ((items & MessageSummaryItems.BodyStructure) != 0) + tokens.Add ("BODYSTRUCTURE"); + if ((items & MessageSummaryItems.Body) != 0) + tokens.Add ("BODY"); + + if ((engine.Capabilities & ImapCapabilities.CondStore) != 0) { + if ((items & MessageSummaryItems.ModSeq) != 0) + tokens.Add ("MODSEQ"); + } + + if ((engine.Capabilities & ImapCapabilities.Annotate) != 0) { + if ((items & MessageSummaryItems.Annotations) != 0) + tokens.Add ("ANNOTATION (/* (value size))"); + } + + if ((engine.Capabilities & ImapCapabilities.ObjectID) != 0) { + if ((items & MessageSummaryItems.EmailId) != 0) + tokens.Add ("EMAILID"); + if ((items & MessageSummaryItems.ThreadId) != 0) + tokens.Add ("THREADID"); + } + + if ((engine.Capabilities & ImapCapabilities.SaveDate) != 0) { + if ((items & MessageSummaryItems.SaveDate) != 0) + tokens.Add ("SAVEDATE"); + } + + if ((engine.Capabilities & ImapCapabilities.Preview) != 0) { + if ((items & MessageSummaryItems.PreviewText) != 0) { +#if ENABLE_LAZY_PREVIEW_API + if (request.PreviewOptions == PreviewOptions.Lazy) + tokens.Add ("PREVIEW (LAZY)"); + else + tokens.Add ("PREVIEW"); +#else + tokens.Add ("PREVIEW"); +#endif + } + } + + if ((engine.Capabilities & ImapCapabilities.GMailExt1) != 0) { + // now for the GMail extension items + if ((items & MessageSummaryItems.GMailMessageId) != 0) + tokens.Add ("X-GM-MSGID"); + if ((items & MessageSummaryItems.GMailThreadId) != 0) + tokens.Add ("X-GM-THRID"); + if ((items & MessageSummaryItems.GMailLabels) != 0) + tokens.Add ("X-GM-LABELS"); + } + + if (request.Headers != null) { + bool requestReferences = (items & MessageSummaryItems.References) != 0; + + if (IsEmptyExclude (request.Headers, requestReferences)) { + tokens.Add ("BODY.PEEK[HEADER]"); + } else if (request.Headers.Exclude) { + var headerFields = new StringBuilder ("BODY.PEEK[HEADER.FIELDS.NOT ("); + + foreach (var header in request.Headers) { + if (requestReferences && header.Equals ("REFERENCES", StringComparison.Ordinal)) + continue; + + headerFields.Append (header); + headerFields.Append (' '); + } + + headerFields[headerFields.Length - 1] = ')'; + headerFields.Append (']'); + + tokens.Add (headerFields.ToString ()); + } else { + var headerFields = new StringBuilder ("BODY.PEEK[HEADER.FIELDS ("); + + foreach (var header in request.Headers) { + headerFields.Append (header); + headerFields.Append (' '); + } + + if (requestReferences && !request.Headers.Contains ("REFERENCES")) + headerFields.Append ("REFERENCES "); + + headerFields[headerFields.Length - 1] = ')'; + headerFields.Append (']'); + + tokens.Add (headerFields.ToString ()); + } + } else if ((items & MessageSummaryItems.Headers) != 0) { + tokens.Add ("BODY.PEEK[HEADER]"); + } else if ((items & MessageSummaryItems.References) != 0) { + tokens.Add ("BODY.PEEK[HEADER.FIELDS (REFERENCES)]"); + } + + if (tokens.Count == 1 && !isNotify) + return tokens[0]; + + return string.Format ("({0})", string.Join (" ", tokens)); + } + + class FetchPreviewTextContext : FetchStreamContextBase + { + static readonly PlainTextPreviewer textPreviewer = new PlainTextPreviewer (); + static readonly HtmlTextPreviewer htmlPreviewer = new HtmlTextPreviewer (); + + readonly FetchSummaryContext ctx; + readonly ImapFolder folder; + + public FetchPreviewTextContext (ImapFolder folder, FetchSummaryContext ctx) : base (null) + { + this.folder = folder; + this.ctx = ctx; + } + + public override void Add (Section section, CancellationToken cancellationToken) + { + if (!ctx.TryGetValue (section.Index, out var message)) + return; + + var body = message.TextBody; + TextPreviewer previewer; + + if (body == null) { + previewer = htmlPreviewer; + body = message.HtmlBody; + } else { + previewer = textPreviewer; + } + + if (body == null) + return; + + var charset = body.ContentType.Charset ?? "utf-8"; + ContentEncoding encoding; + + if (string.IsNullOrEmpty (body.ContentTransferEncoding) || !MimeUtils.TryParse (body.ContentTransferEncoding, out encoding)) + encoding = ContentEncoding.Default; + + using (var memory = new MemoryStream ()) { + var content = new MimeContent (section.Stream, encoding); + + content.DecodeTo (memory, cancellationToken); + memory.Position = 0; + + try { + message.PreviewText = previewer.GetPreviewText (memory, charset); + } catch (DecoderFallbackException) { + memory.Position = 0; + + message.PreviewText = previewer.GetPreviewText (memory, TextEncodings.Latin1); + } + + message.Fields |= MessageSummaryItems.PreviewText; + folder.OnMessageSummaryFetched (message); + } + + return; + } + + public override void SetUniqueId (int index, UniqueId uid, CancellationToken cancellationToken) + { + // no-op + } + } + + void ProcessFetchResponse (ImapCommand ic) + { + ProcessResponseCodes (ic, null); + + ic.ThrowIfNotOk ("FETCH"); + } + + ImapCommand QueueFetchPreviewTextCommand (FetchSummaryContext sctx, KeyValuePair pair, int octets, CancellationToken cancellationToken) + { + var uids = pair.Value; + string specifier; + + if (!string.IsNullOrEmpty (pair.Key)) + specifier = pair.Key; + else + specifier = "TEXT"; + + // TODO: if the IMAP server supports the CONVERT extension, we could possibly use the + // CONVERT command instead to decode *and* convert (html) into utf-8 plain text. + // + // e.g. "UID CONVERT {0} (\"text/plain\" (\"charset\" \"utf-8\")) BINARY[{1}]<0.{2}>\r\n" + // + // This would allow us to more accurately fetch X number of characters because we wouldn't + // need to guesstimate accounting for base64/quoted-printable decoding. + + var command = string.Format (CultureInfo.InvariantCulture, "UID FETCH {0} (BODY.PEEK[{1}]<0.{2}>)\r\n", uids, specifier, octets); + var ic = new ImapCommand (Engine, cancellationToken, this, command); + var ctx = new FetchPreviewTextContext (this, sctx); + + ic.RegisterUntaggedHandler ("FETCH", FetchStreamHandler); + ic.UserData = ctx; + + Engine.QueueCommand (ic); + + return ic; + } + + void FetchPreviewText (FetchSummaryContext sctx, Dictionary bodies, int octets, CancellationToken cancellationToken) + { + foreach (var pair in bodies) { + var ic = QueueFetchPreviewTextCommand (sctx, pair, octets, cancellationToken); + var ctx = (FetchPreviewTextContext) ic.UserData!; + + try { + Engine.Run (ic); + + ProcessFetchResponse (ic); + } finally { + ctx.Dispose (); + } + } + } + + async Task FetchPreviewTextAsync (FetchSummaryContext sctx, Dictionary bodies, int octets, CancellationToken cancellationToken) + { + foreach (var pair in bodies) { + var ic = QueueFetchPreviewTextCommand (sctx, pair, octets, cancellationToken); + var ctx = (FetchPreviewTextContext) ic.UserData!; + + try { + await Engine.RunAsync (ic).ConfigureAwait (false); + + ProcessFetchResponse (ic); + } finally { + ctx.Dispose (); + } + } + } + + void CreateFetchPreviewTextMappings (FetchSummaryContext sctx, out Dictionary textBodies, out Dictionary htmlBodies) + { + textBodies = new Dictionary (); + htmlBodies = new Dictionary (); + + foreach (var item in sctx.Messages) { + Dictionary bodies; + var message = (MessageSummary) item; + var body = message.TextBody; + + if (body == null) { + body = message.HtmlBody; + bodies = htmlBodies; + } else { + bodies = textBodies; + } + + if (body == null || body.Octets == 0) { + message.Fields |= MessageSummaryItems.PreviewText; + message.PreviewText = string.Empty; + OnMessageSummaryFetched (message); + continue; + } + + if (!bodies.TryGetValue (body.PartSpecifier, out var uids)) { + uids = new UniqueIdSet (SortOrder.Ascending); + bodies.Add (body.PartSpecifier, uids); + } + + uids.Add (message.UniqueId); + } + } + + void GetPreviewText (FetchSummaryContext sctx, CancellationToken cancellationToken) + { + CreateFetchPreviewTextMappings (sctx, out var textBodies, out var htmlBodies); + + MessageExpunged += sctx.OnMessageExpunged; + + try { + FetchPreviewText (sctx, textBodies, PreviewTextLength, cancellationToken); + FetchPreviewText (sctx, htmlBodies, PreviewHtmlLength, cancellationToken); + } finally { + MessageExpunged -= sctx.OnMessageExpunged; + } + } + + async Task GetPreviewTextAsync (FetchSummaryContext sctx, CancellationToken cancellationToken) + { + CreateFetchPreviewTextMappings (sctx, out var textBodies, out var htmlBodies); + + MessageExpunged += sctx.OnMessageExpunged; + + try { + await FetchPreviewTextAsync (sctx, textBodies, PreviewTextLength, cancellationToken).ConfigureAwait (false); + await FetchPreviewTextAsync (sctx, htmlBodies, PreviewHtmlLength, cancellationToken).ConfigureAwait (false); + } finally { + MessageExpunged -= sctx.OnMessageExpunged; + } + } + + internal static bool IsEmptyFetchRequest (IFetchRequest request) + { + return request.Items == MessageSummaryItems.None && (request.Headers == null || (request.Headers.Count == 0 && !request.Headers.Exclude)); + } + + bool CheckCanFetch (IList uids, IFetchRequest request) + { + if (uids == null) + throw new ArgumentNullException (nameof (uids)); + + if (request == null) + throw new ArgumentNullException (nameof (request)); + + if (request.ChangedSince.HasValue && !supportsModSeq) + throw new NotSupportedException ("The ImapFolder does not support mod-sequences."); + + CheckState (true, false); + + return uids.Count > 0 && !IsEmptyFetchRequest (request); + } + + string CreateFetchCommand (IList uids, IFetchRequest request, out bool previewText) + { + var query = FormatSummaryItems (Engine, request, out previewText); + var changedSince = string.Empty; + + if (request.ChangedSince.HasValue) { + var vanished = Engine.QResyncEnabled ? " VANISHED" : string.Empty; + + changedSince = string.Format (CultureInfo.InvariantCulture, " (CHANGEDSINCE {0}{1})", request.ChangedSince.Value, vanished); + } + + return string.Format ("UID FETCH %s {0}{1}\r\n", query, changedSince); + } + + static int EstimateInitialCapacity (IList uids) + { + if (uids is UniqueIdRange || uids is UniqueIdSet) { + // UniqueIdRange is likely to refer to UIDs that have not yet been assigned or have been expunged, + // so cap our maximum initial capacity to 1024 (a reasonable limit?). + return Math.Min (uids.Count, 1024); + } + + // If the user supplied an exact set of UIDs, then we'll assume they all exist + // and therefore we can use the capacity of `uids` as our initial capacity. + return uids.Count; + } + + /// + /// Fetches the message summaries for the specified message UIDs. + /// + /// + /// Fetches the message summaries for the specified message UIDs. + /// It should be noted that if another client has modified any message + /// in the folder, the IMAP server may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// + /// + /// + /// An enumeration of summaries for the requested messages. + /// The UIDs. + /// The fetch request. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not currently open. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override IList Fetch (IList uids, IFetchRequest request, CancellationToken cancellationToken = default) + { + if (!CheckCanFetch (uids, request)) + return Array.Empty (); + + var command = CreateFetchCommand (uids, request, out bool previewText); + var ctx = new FetchSummaryContext (EstimateInitialCapacity (uids)); + + MessageExpunged += ctx.OnMessageExpunged; + + try { + foreach (var ic in Engine.CreateCommands (cancellationToken, this, command, uids)) { + ic.RegisterUntaggedHandler ("FETCH", UntaggedFetchSummaryItemsHandler); + ic.UserData = ctx; + + Engine.QueueCommand (ic); + + Engine.Run (ic); + + ProcessFetchResponse (ic); + } + } finally { + MessageExpunged -= ctx.OnMessageExpunged; + } + + if (previewText) + GetPreviewText (ctx, cancellationToken); + + return ctx.Messages.AsReadOnly (); + } + + /// + /// Asynchronously fetches the message summaries for the specified message UIDs. + /// + /// + /// Fetches the message summaries for the specified message UIDs. + /// It should be noted that if another client has modified any message + /// in the folder, the IMAP server may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// + /// + /// + /// An enumeration of summaries for the requested messages. + /// The UIDs. + /// The fetch request. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not currently open. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override async Task> FetchAsync (IList uids, IFetchRequest request, CancellationToken cancellationToken = default) + { + if (!CheckCanFetch (uids, request)) + return Array.Empty (); + + var command = CreateFetchCommand (uids, request, out bool previewText); + var ctx = new FetchSummaryContext (EstimateInitialCapacity (uids)); + + MessageExpunged += ctx.OnMessageExpunged; + + try { + foreach (var ic in Engine.CreateCommands (cancellationToken, this, command, uids)) { + ic.RegisterUntaggedHandler ("FETCH", UntaggedFetchSummaryItemsHandler); + ic.UserData = ctx; + + Engine.QueueCommand (ic); + + await Engine.RunAsync (ic).ConfigureAwait (false); + + ProcessFetchResponse (ic); + } + } finally { + MessageExpunged -= ctx.OnMessageExpunged; + } + + if (previewText) + await GetPreviewTextAsync (ctx, cancellationToken).ConfigureAwait (false); + + return ctx.Messages.AsReadOnly (); + } + + bool CheckCanFetch (IList indexes, IFetchRequest request) + { + if (indexes == null) + throw new ArgumentNullException (nameof (indexes)); + + if (request == null) + throw new ArgumentNullException (nameof (request)); + + if (request.ChangedSince.HasValue && !supportsModSeq) + throw new NotSupportedException ("The ImapFolder does not support mod-sequences."); + + CheckState (true, false); + CheckAllowIndexes (); + + return indexes.Count > 0 && !IsEmptyFetchRequest (request); + } + + ImapCommand QueueFetchCommand (IList indexes, IFetchRequest request, CancellationToken cancellationToken, out bool previewText) + { + var query = FormatSummaryItems (Engine, request, out previewText); + var set = ImapUtils.FormatIndexSet (Engine, indexes); + var changedSince = string.Empty; + + if (request.ChangedSince.HasValue) + changedSince = string.Format (CultureInfo.InvariantCulture, " (CHANGEDSINCE {0})", request.ChangedSince.Value); + + var command = string.Format ("FETCH {0} {1}{2}\r\n", set, query, changedSince); + var ic = new ImapCommand (Engine, cancellationToken, this, command); + var ctx = new FetchSummaryContext (indexes.Count); + + ic.RegisterUntaggedHandler ("FETCH", UntaggedFetchSummaryItemsHandler); + ic.UserData = ctx; + + Engine.QueueCommand (ic); + + return ic; + } + + /// + /// Fetches the message summaries for the specified message indexes. + /// + /// + /// Fetches the message summaries for the specified message indexes. + /// It should be noted that if another client has modified any message + /// in the folder, the IMAP server may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The indexes. + /// The fetch request. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not currently open. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override IList Fetch (IList indexes, IFetchRequest request, CancellationToken cancellationToken = default) + { + if (!CheckCanFetch (indexes, request)) + return Array.Empty (); + + var ic = QueueFetchCommand (indexes, request, cancellationToken, out bool previewText); + var ctx = (FetchSummaryContext) ic.UserData!; + + Engine.Run (ic); + + ProcessFetchResponse (ic); + + if (previewText) + GetPreviewText (ctx, cancellationToken); + + return ctx.Messages.AsReadOnly (); + } + + /// + /// Asynchronously fetches the message summaries for the specified message indexes. + /// + /// + /// Fetches the message summaries for the specified message indexes. + /// It should be noted that if another client has modified any message + /// in the folder, the IMAP server may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The indexes. + /// The fetch request. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not currently open. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override async Task> FetchAsync (IList indexes, IFetchRequest request, CancellationToken cancellationToken = default) + { + if (!CheckCanFetch (indexes, request)) + return Array.Empty (); + + var ic = QueueFetchCommand (indexes, request, cancellationToken, out bool previewText); + var ctx = (FetchSummaryContext) ic.UserData!; + + await Engine.RunAsync (ic).ConfigureAwait (false); + + ProcessFetchResponse (ic); + + if (previewText) + await GetPreviewTextAsync (ctx, cancellationToken).ConfigureAwait (false); + + return ctx.Messages.AsReadOnly (); + } + + static string GetFetchRange (int min, int max) + { + var minValue = (min + 1).ToString (CultureInfo.InvariantCulture); + + if (min == max) + return minValue; + + var maxValue = max != -1 ? (max + 1).ToString (CultureInfo.InvariantCulture) : "*"; + + return string.Format (CultureInfo.InvariantCulture, "{0}:{1}", minValue, maxValue); + } + + bool CheckCanFetch (int min, int max, IFetchRequest request) + { + if (min < 0) + throw new ArgumentOutOfRangeException (nameof (min)); + + if (max != -1 && max < min) + throw new ArgumentOutOfRangeException (nameof (max)); + + if (request == null) + throw new ArgumentNullException (nameof (request)); + + if (request.ChangedSince.HasValue && !supportsModSeq) + throw new NotSupportedException ("The ImapFolder does not support mod-sequences."); + + CheckState (true, false); + CheckAllowIndexes (); + + return Count > 0 && !IsEmptyFetchRequest (request); + } + + ImapCommand QueueFetchCommand (int min, int max, IFetchRequest request, CancellationToken cancellationToken, out bool previewText) + { + int capacity = Math.Max (max < 0 || max > Count ? Count : max, min) - min; + var query = FormatSummaryItems (Engine, request, out previewText); + var set = GetFetchRange (min, max); + var changedSince = string.Empty; + + if (request.ChangedSince.HasValue) + changedSince = string.Format (CultureInfo.InvariantCulture, " (CHANGEDSINCE {0})", request.ChangedSince.Value); + + var command = string.Format ("FETCH {0} {1}{2}\r\n", set, query, changedSince); + var ic = new ImapCommand (Engine, cancellationToken, this, command); + var ctx = new FetchSummaryContext (capacity); + + ic.RegisterUntaggedHandler ("FETCH", UntaggedFetchSummaryItemsHandler); + ic.UserData = ctx; + + Engine.QueueCommand (ic); + + return ic; + } + + /// + /// Fetches the message summaries for the messages between the two indexes, inclusive. + /// + /// + /// Fetches the message summaries for the messages between the two + /// indexes, inclusive. + /// It should be noted that if another client has modified any message + /// in the folder, the IMAP server may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// The fetch request. + /// The cancellation token. + /// + /// is out of range. + /// -or- + /// is out of range. + /// + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not currently open. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override IList Fetch (int min, int max, IFetchRequest request, CancellationToken cancellationToken = default) + { + if (!CheckCanFetch (min, max, request)) + return Array.Empty (); + + var ic = QueueFetchCommand (min, max, request, cancellationToken, out bool previewText); + var ctx = (FetchSummaryContext) ic.UserData!; + + Engine.Run (ic); + + ProcessFetchResponse (ic); + + if (previewText) + GetPreviewText (ctx, cancellationToken); + + return ctx.Messages.AsReadOnly (); + } + + /// + /// Asynchronously fetches the message summaries for the messages between the two indexes, inclusive. + /// + /// + /// Fetches the message summaries for the messages between the two + /// indexes, inclusive. + /// It should be noted that if another client has modified any message + /// in the folder, the IMAP server may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// The fetch request. + /// The cancellation token. + /// + /// is out of range. + /// -or- + /// is out of range. + /// + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not currently open. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override async Task> FetchAsync (int min, int max, IFetchRequest request, CancellationToken cancellationToken = default) + { + if (!CheckCanFetch (min, max, request)) + return Array.Empty (); + + var ic = QueueFetchCommand (min, max, request, cancellationToken, out bool previewText); + var ctx = (FetchSummaryContext) ic.UserData!; + + await Engine.RunAsync (ic).ConfigureAwait (false); + + ProcessFetchResponse (ic); + + if (previewText) + await GetPreviewTextAsync (ctx, cancellationToken).ConfigureAwait (false); + + return ctx.Messages.AsReadOnly (); + } + + /// + /// Create a backing stream for use with the GetMessage, GetBodyPart, and GetStream methods. + /// + /// + /// Allows subclass implementations to override the type of stream + /// created for use with the GetMessage, GetBodyPart and GetStream methods. + /// This could be useful for subclass implementations that intend to implement + /// support for caching and/or for subclass implementations that want to use + /// temporary file streams instead of memory-based streams for larger amounts of + /// message data. + /// Subclasses that implement caching using this API should wait for + /// before adding the stream to their cache. + /// Streams returned by this method SHOULD clean up any allocated resources + /// such as deleting temporary files from the file system. + /// The will not be available for the various + /// GetMessage(), GetBodyPart() and GetStream() methods that take a message index rather + /// than a . It may also not be available if the IMAP server + /// response does not specify the UID value prior to sending the literal-string + /// token containing the message stream. + /// + /// + /// The stream. + /// The unique identifier of the message, if available. + /// The section of the message that is being fetched. + /// The starting offset of the message section being fetched. + /// The length of the stream being fetched, measured in bytes. + protected virtual Stream CreateStream (UniqueId? uid, string section, int offset, int length) + { + if (length > 4096) + return new MemoryBlockStream (); + + return new MemoryStream (length); + } + + /// + /// Commit a stream returned by . + /// + /// + /// Commits a stream returned by . + /// This method is called only after both the message data has successfully + /// been written to the stream returned by and a + /// has been obtained for the associated message. + /// For subclasses implementing caching, this method should be used for + /// committing the stream to their cache. + /// Subclass implementations may take advantage of the fact that + /// allows returning a new + /// reference if they move a file on the file system and wish to return a new + /// based on the new path, for example. + /// + /// + /// The stream. + /// The stream. + /// The unique identifier of the message. + /// The section of the message that the stream represents. + /// The starting offset of the message section. + /// The length of the stream, measured in bytes. + protected virtual Stream CommitStream (Stream stream, UniqueId uid, string section, int offset, int length) + { + return stream; + } + + HeaderList ParseHeaders (Stream stream, CancellationToken cancellationToken) + { + try { + return Engine.ParseHeaders (stream, cancellationToken); + } finally { + stream.Dispose (); + } + } + + async Task ParseHeadersAsync (Stream stream, CancellationToken cancellationToken) + { + try { + return await Engine.ParseHeadersAsync (stream, cancellationToken).ConfigureAwait (false); + } finally { + stream.Dispose (); + } + } + + MimeMessage ParseMessage (Stream stream, CancellationToken cancellationToken) + { + bool dispose = !(stream is MemoryStream || stream is MemoryBlockStream); + + try { + return Engine.ParseMessage (stream, !dispose, cancellationToken); + } finally { + if (dispose) + stream.Dispose (); + } + } + + async Task ParseMessageAsync (Stream stream, CancellationToken cancellationToken) + { + bool dispose = !(stream is MemoryStream || stream is MemoryBlockStream); + + try { + return await Engine.ParseMessageAsync (stream, !dispose, cancellationToken).ConfigureAwait (false); + } finally { + if (dispose) + stream.Dispose (); + } + } + + MimeEntity ParseEntity (Stream stream, bool dispose, CancellationToken cancellationToken) + { + try { + return Engine.ParseEntity (stream, !dispose, cancellationToken); + } finally { + if (dispose) + stream.Dispose (); + } + } + + async Task ParseEntityAsync (Stream stream, bool dispose, CancellationToken cancellationToken) + { + try { + return await Engine.ParseEntityAsync (stream, !dispose, cancellationToken).ConfigureAwait (false); + } finally { + if (dispose) + stream.Dispose (); + } + } + + class Section + { + public readonly int Index; + public UniqueId? UniqueId; + public readonly Stream Stream; + public readonly string Name; + public readonly int Offset; + public readonly int Length; + + public Section (Stream stream, int index, UniqueId? uid, string name, int offset, int length) + { + Stream = stream; + Offset = offset; + Length = length; + UniqueId = uid; + Index = index; + Name = name; + } + } + + abstract class FetchStreamContextBase : IDisposable + { + public readonly List
Sections = new List
(); + readonly ITransferProgress? progress; + + protected FetchStreamContextBase (ITransferProgress? progress) + { + this.progress = progress; + } + + public abstract void Add (Section section, CancellationToken cancellationToken); + + public virtual Task AddAsync (Section section, CancellationToken cancellationToken) + { + Add (section, cancellationToken); + + return Task.CompletedTask; + } + + public virtual bool Contains (int index, string specifier, [NotNullWhen (true)] out Section? section) + { + section = null; + return false; + } + + public abstract void SetUniqueId (int index, UniqueId uid, CancellationToken cancellationToken); + + public virtual Task SetUniqueIdAsync (int index, UniqueId uid, CancellationToken cancellationToken) + { + SetUniqueId (index, uid, cancellationToken); + + return Task.CompletedTask; + } + + public void Report (long nread, long total) + { + if (progress == null) + return; + + progress.Report (nread, total); + } + + public void Dispose () + { + for (int i = 0; i < Sections.Count; i++) { + var section = Sections[i]; + + try { + section.Stream.Dispose (); + } catch (IOException) { + } + } + } + } + + class FetchStreamContext : FetchStreamContextBase + { + public FetchStreamContext (ITransferProgress? progress) : base (progress) + { + } + + public override void Add (Section section, CancellationToken cancellationToken) + { + Sections.Add (section); + } + + public bool TryGetSection (UniqueId uid, string specifier, [NotNullWhen (true)] out Section? section, bool remove = false) + { + for (int i = 0; i < Sections.Count; i++) { + var item = Sections[i]; + + if (!item.UniqueId.HasValue || item.UniqueId.Value != uid) + continue; + + if (item.Name.Equals (specifier, StringComparison.OrdinalIgnoreCase)) { + if (remove) + Sections.RemoveAt (i); + + section = item; + return true; + } + } + + section = null; + + return false; + } + + public bool TryGetSection (int index, string specifier, [NotNullWhen (true)] out Section? section, bool remove = false) + { + for (int i = 0; i < Sections.Count; i++) { + var item = Sections[i]; + + if (item.Index != index) + continue; + + if (item.Name.Equals (specifier, StringComparison.OrdinalIgnoreCase)) { + if (remove) + Sections.RemoveAt (i); + + section = item; + return true; + } + } + + section = null; + + return false; + } + + public override void SetUniqueId (int index, UniqueId uid, CancellationToken cancellationToken) + { + for (int i = 0; i < Sections.Count; i++) { + if (Sections[i].Index == index) + Sections[i].UniqueId = uid; + } + } + } + + void FetchStream (ImapEngine engine, ImapCommand ic, int index) + { + var token = engine.ReadToken (ic.CancellationToken); + var ctx = (FetchStreamContextBase) ic.UserData!; + var sectionBuilder = new StringBuilder (); + IList? annotations = null; + MessageFlags flags = MessageFlags.None; + HashSet? keywords = null; + IList? labels = null; + long nread = 0, size = 0; + UniqueId? uid = null; + ulong? modseq = null; + Stream stream; + string name; + byte[] buf; + int n; + + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "FETCH", token); + + do { + token = engine.ReadToken (ic.CancellationToken); + + if (token.Type == ImapTokenType.Eoln) { + // Note: Most likely the message body was calculated to be 1 or 2 bytes too + // short (e.g. did not include the trailing ) and that is the EOLN we just + // reached. Ignore it and continue as normal. + // + // See https://github.com/jstedfast/MailKit/issues/954 for details. + token = engine.ReadToken (ic.CancellationToken); + } + + if (token.Type == ImapTokenType.CloseParen) + break; + + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "FETCH", token); + + var atom = (string) token.Value; + int offset = 0, length; + uint value; + + if (atom.Equals ("BODY", StringComparison.OrdinalIgnoreCase)) { + token = engine.ReadToken (ic.CancellationToken); + + ImapEngine.AssertToken (token, ImapTokenType.OpenBracket, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + + sectionBuilder.Clear (); + + do { + token = engine.ReadToken (ic.CancellationToken); + + if (token.Type == ImapTokenType.CloseBracket) + break; + + if (token.Type == ImapTokenType.OpenParen) { + sectionBuilder.Append (" ("); + + do { + token = engine.ReadToken (ic.CancellationToken); + + if (token.Type == ImapTokenType.CloseParen) + break; + + // the header field names will generally be atoms or qstrings but may also be literals + engine.UngetToken (token); + + var field = ImapUtils.ReadStringToken (engine, ImapEngine.FetchBodySyntaxErrorFormat, ic.CancellationToken); + + sectionBuilder.Append (field); + sectionBuilder.Append (' '); + } while (true); + + if (sectionBuilder[sectionBuilder.Length - 1] == ' ') + sectionBuilder.Length--; + + sectionBuilder.Append (')'); + } else { + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + + sectionBuilder.Append ((string) token.Value); + } + } while (true); + + ImapEngine.AssertToken (token, ImapTokenType.CloseBracket, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + + token = engine.ReadToken (ic.CancellationToken); + + if (token.Type == ImapTokenType.Atom) { + // this might be a region ("<###>") + var expr = (string) token.Value; + + if (expr.Length > 2 && expr[0] == '<' && expr[expr.Length - 1] == '>') { + var region = expr.Substring (1, expr.Length - 2); + + int.TryParse (region, NumberStyles.None, CultureInfo.InvariantCulture, out offset); + + token = engine.ReadToken (ic.CancellationToken); + } + } + + name = sectionBuilder.ToString (); + + switch (token.Type) { + case ImapTokenType.Literal: + length = (int) token.Value; + size += length; + + stream = CreateStream (uid, name, offset, length); + + buf = ArrayPool.Shared.Rent (BufferSize); + + try { + do { + n = engine.Stream!.Read (buf, 0, BufferSize, ic.CancellationToken); + + if (n > 0) { + stream.Write (buf, 0, n); + nread += n; + + ctx.Report (nread, size); + } else { + break; + } + } while (true); + + stream.Position = 0; + } catch { + stream?.Dispose (); + throw; + } finally { + ArrayPool.Shared.Return (buf); + } + break; + case ImapTokenType.QString: + case ImapTokenType.Atom: + buf = Encoding.UTF8.GetBytes ((string) token.Value); + length = buf.Length; + nread += length; + size += length; + + stream = CreateStream (uid, name, offset, length); + + try { + stream.Write (buf, 0, length); + ctx.Report (nread, size); + stream.Position = 0; + } catch { + stream?.Dispose (); + throw; + } + break; + case ImapTokenType.Nil: + stream = CreateStream (uid, name, offset, 0); + length = 0; + break; + case ImapTokenType.CloseParen: + // Note: Yandex IMAP servers sometimes do not include the BODY[
] content value in the FETCH response. + // + // See https://github.com/jstedfast/MailKit/issues/1708 for details. + // + // Unget the ')' token and pretend we got a NIL token. + engine.UngetToken (token); + goto case ImapTokenType.Nil; + default: + throw ImapEngine.UnexpectedToken (ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + } + + if (uid.HasValue) + stream = CommitStream (stream, uid.Value, name, offset, length); + + // prevent leaks in the (invalid) case where a section may be returned twice + if (ctx.Contains (index, name, out var section)) + section.Stream.Dispose (); + + section = new Section (stream, index, uid, name, offset, length); + ctx.Add (section, ic.CancellationToken); + } else if (atom.Equals ("UID", StringComparison.OrdinalIgnoreCase)) { + token = engine.ReadToken (ic.CancellationToken); + + value = ImapEngine.ParseNumber (token, true, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + uid = new UniqueId (UidValidity, value); + + ctx.SetUniqueId (index, uid.Value, ic.CancellationToken); + } else if (atom.Equals ("MODSEQ", StringComparison.OrdinalIgnoreCase)) { + token = engine.ReadToken (ic.CancellationToken); + + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + + token = engine.ReadToken (ic.CancellationToken); + + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + + // Note: Some IMAP servers (such as Zoho Mail) will return a MODSEQ of -1 in some cases (not sure why). + // + // If we get an invalid value, just ignore it. + // + // See https://github.com/jstedfast/MailKit/issues/1686 for details. + if (ImapEngine.TryParseNumber64 (token, out ulong n64)) { + if (n64 > HighestModSeq) + UpdateHighestModSeq (n64); + + modseq = n64; + } + + token = engine.ReadToken (ic.CancellationToken); + + ImapEngine.AssertToken (token, ImapTokenType.CloseParen, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + } else if (atom.Equals ("FLAGS", StringComparison.OrdinalIgnoreCase)) { + // even though we didn't request this piece of information, the IMAP server + // may send it if another client has recently modified the message flags. + keywords = new HashSet (StringComparer.Ordinal); + flags = ImapUtils.ParseFlagsList (engine, atom, keywords, ic.CancellationToken); + } else if (atom.Equals ("X-GM-LABELS", StringComparison.OrdinalIgnoreCase)) { + // even though we didn't request this piece of information, the IMAP server + // may send it if another client has recently modified the message labels. + labels = ImapUtils.ParseLabelsList (engine, ic.CancellationToken); + } else if (atom.Equals ("ANNOTATION", StringComparison.OrdinalIgnoreCase)) { + // even though we didn't request this piece of information, the IMAP server + // may send it if another client has recently modified the message annotations. + annotations = ImapUtils.ParseAnnotations (engine, ic.CancellationToken); + } else { + // Unexpected or unknown token (such as XAOL.SPAM.REASON or XAOL-MSGID). Simply read 1 more token (the argument) and ignore. + token = engine.ReadToken (ic.CancellationToken); + + if (token.Type == ImapTokenType.OpenParen) + SkipParenthesizedList (engine, ic.CancellationToken); + } + } while (true); + + ImapEngine.AssertToken (token, ImapTokenType.CloseParen, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "FETCH", token); + + if (keywords != null) + OnMessageFlagsChanged (new MessageFlagsChangedEventArgs (index, flags, keywords) { UniqueId = uid, ModSeq = modseq }); + + if (labels != null) + OnMessageLabelsChanged (new MessageLabelsChangedEventArgs (index, labels) { UniqueId = uid, ModSeq = modseq }); + + if (annotations != null) + OnAnnotationsChanged (new AnnotationsChangedEventArgs (index, annotations) { UniqueId = uid, ModSeq = modseq }); + + if (modseq.HasValue) + OnModSeqChanged (new ModSeqChangedEventArgs (index, modseq.Value) { UniqueId = uid }); + } + + async Task FetchStreamAsync (ImapEngine engine, ImapCommand ic, int index) + { + var token = await engine.ReadTokenAsync (ic.CancellationToken).ConfigureAwait (false); + var ctx = (FetchStreamContextBase) ic.UserData!; + var sectionBuilder = new StringBuilder (); + IList? annotations = null; + MessageFlags flags = MessageFlags.None; + HashSet? keywords = null; + IList? labels = null; + long nread = 0, size = 0; + UniqueId? uid = null; + ulong? modseq = null; + Stream stream; + string name; + byte[] buf; + int n; + + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "FETCH", token); + + do { + token = await engine.ReadTokenAsync (ic.CancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.Eoln) { + // Note: Most likely the message body was calculated to be 1 or 2 bytes too + // short (e.g. did not include the trailing ) and that is the EOLN we just + // reached. Ignore it and continue as normal. + // + // See https://github.com/jstedfast/MailKit/issues/954 for details. + token = await engine.ReadTokenAsync (ic.CancellationToken).ConfigureAwait (false); + } + + if (token.Type == ImapTokenType.CloseParen) + break; + + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "FETCH", token); + + var atom = (string) token.Value; + int offset = 0, length; + uint value; + + if (atom.Equals ("BODY", StringComparison.OrdinalIgnoreCase)) { + token = await engine.ReadTokenAsync (ic.CancellationToken).ConfigureAwait (false); + + ImapEngine.AssertToken (token, ImapTokenType.OpenBracket, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + + sectionBuilder.Clear (); + + do { + token = await engine.ReadTokenAsync (ic.CancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.CloseBracket) + break; + + if (token.Type == ImapTokenType.OpenParen) { + sectionBuilder.Append (" ("); + + do { + token = await engine.ReadTokenAsync (ic.CancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.CloseParen) + break; + + // the header field names will generally be atoms or qstrings but may also be literals + engine.UngetToken (token); + + var field = await ImapUtils.ReadStringTokenAsync (engine, ImapEngine.FetchBodySyntaxErrorFormat, ic.CancellationToken).ConfigureAwait (false); + + sectionBuilder.Append (field); + sectionBuilder.Append (' '); + } while (true); + + if (sectionBuilder[sectionBuilder.Length - 1] == ' ') + sectionBuilder.Length--; + + sectionBuilder.Append (')'); + } else { + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + + sectionBuilder.Append ((string) token.Value); + } + } while (true); + + ImapEngine.AssertToken (token, ImapTokenType.CloseBracket, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + + token = await engine.ReadTokenAsync (ic.CancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.Atom) { + // this might be a region ("<###>") + var expr = (string) token.Value; + + if (expr.Length > 2 && expr[0] == '<' && expr[expr.Length - 1] == '>') { + var region = expr.Substring (1, expr.Length - 2); + + int.TryParse (region, NumberStyles.None, CultureInfo.InvariantCulture, out offset); + + token = await engine.ReadTokenAsync (ic.CancellationToken).ConfigureAwait (false); + } + } + + name = sectionBuilder.ToString (); + + switch (token.Type) { + case ImapTokenType.Literal: + length = (int) token.Value; + size += length; + + stream = CreateStream (uid, name, offset, length); + + buf = ArrayPool.Shared.Rent (BufferSize); + + try { + do { + n = await engine.Stream!.ReadAsync (buf, 0, BufferSize, ic.CancellationToken).ConfigureAwait (false); + + if (n > 0) { + stream.Write (buf, 0, n); + nread += n; + + ctx.Report (nread, size); + } else { + break; + } + } while (true); + + stream.Position = 0; + } catch { + stream?.Dispose (); + throw; + } finally { + ArrayPool.Shared.Return (buf); + } + break; + case ImapTokenType.QString: + case ImapTokenType.Atom: + buf = Encoding.UTF8.GetBytes ((string) token.Value); + length = buf.Length; + nread += length; + size += length; + + stream = CreateStream (uid, name, offset, length); + + try { + stream.Write (buf, 0, length); + ctx.Report (nread, size); + stream.Position = 0; + } catch { + stream?.Dispose (); + throw; + } + break; + case ImapTokenType.Nil: + stream = CreateStream (uid, name, offset, 0); + length = 0; + break; + case ImapTokenType.CloseParen: + // Note: Yandex IMAP servers sometimes do not include the BODY[
] content value in the FETCH response. + // + // See https://github.com/jstedfast/MailKit/issues/1708 for details. + // + // Unget the ')' token and pretend we got a NIL token. + engine.UngetToken (token); + goto case ImapTokenType.Nil; + default: + throw ImapEngine.UnexpectedToken (ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + } + + if (uid.HasValue) + stream = CommitStream (stream, uid.Value, name, offset, length); + + // prevent leaks in the (invalid) case where a section may be returned twice + if (ctx.Contains (index, name, out var section)) + section.Stream.Dispose (); + + section = new Section (stream, index, uid, name, offset, length); + await ctx.AddAsync (section, ic.CancellationToken).ConfigureAwait (false); + } else if (atom.Equals ("UID", StringComparison.OrdinalIgnoreCase)) { + token = await engine.ReadTokenAsync (ic.CancellationToken).ConfigureAwait (false); + + value = ImapEngine.ParseNumber (token, true, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + uid = new UniqueId (UidValidity, value); + + await ctx.SetUniqueIdAsync (index, uid.Value, ic.CancellationToken).ConfigureAwait (false); + } else if (atom.Equals ("MODSEQ", StringComparison.OrdinalIgnoreCase)) { + token = await engine.ReadTokenAsync (ic.CancellationToken).ConfigureAwait (false); + + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + + token = await engine.ReadTokenAsync (ic.CancellationToken).ConfigureAwait (false); + + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + + // Note: Some IMAP servers (such as Zoho Mail) will return a MODSEQ of -1 in some cases (not sure why). + // + // If we get an invalid value, just ignore it. + // + // See https://github.com/jstedfast/MailKit/issues/1686 for details. + if (ImapEngine.TryParseNumber64 (token, out ulong n64)) { + if (n64 > HighestModSeq) + UpdateHighestModSeq (n64); + + modseq = n64; + } + + token = await engine.ReadTokenAsync (ic.CancellationToken).ConfigureAwait (false); + + ImapEngine.AssertToken (token, ImapTokenType.CloseParen, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + } else if (atom.Equals ("FLAGS", StringComparison.OrdinalIgnoreCase)) { + // even though we didn't request this piece of information, the IMAP server + // may send it if another client has recently modified the message flags. + keywords = new HashSet (StringComparer.Ordinal); + flags = await ImapUtils.ParseFlagsListAsync (engine, atom, keywords, ic.CancellationToken).ConfigureAwait (false); + } else if (atom.Equals ("X-GM-LABELS", StringComparison.OrdinalIgnoreCase)) { + // even though we didn't request this piece of information, the IMAP server + // may send it if another client has recently modified the message labels. + labels = await ImapUtils.ParseLabelsListAsync (engine, ic.CancellationToken).ConfigureAwait (false); + } else if (atom.Equals ("ANNOTATION", StringComparison.OrdinalIgnoreCase)) { + // even though we didn't request this piece of information, the IMAP server + // may send it if another client has recently modified the message annotations. + annotations = await ImapUtils.ParseAnnotationsAsync (engine, ic.CancellationToken).ConfigureAwait (false); + } else { + // Unexpected or unknown token (such as XAOL.SPAM.REASON or XAOL-MSGID). Simply read 1 more token (the argument) and ignore. + token = await engine.ReadTokenAsync (ic.CancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.OpenParen) + await SkipParenthesizedListAsync (engine, ic.CancellationToken).ConfigureAwait (false); + } + } while (true); + + ImapEngine.AssertToken (token, ImapTokenType.CloseParen, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "FETCH", token); + + if (keywords != null) + OnMessageFlagsChanged (new MessageFlagsChangedEventArgs (index, flags, keywords) { UniqueId = uid, ModSeq = modseq }); + + if (labels != null) + OnMessageLabelsChanged (new MessageLabelsChangedEventArgs (index, labels) { UniqueId = uid, ModSeq = modseq }); + + if (annotations != null) + OnAnnotationsChanged (new AnnotationsChangedEventArgs (index, annotations) { UniqueId = uid, ModSeq = modseq }); + + if (modseq.HasValue) + OnModSeqChanged (new ModSeqChangedEventArgs (index, modseq.Value) { UniqueId = uid }); + } + + Task FetchStreamHandler (ImapEngine engine, ImapCommand ic, int index, bool doAsync) + { + if (doAsync) + return FetchStreamAsync (engine, ic, index); + + FetchStream (engine, ic, index); + + return Task.CompletedTask; + } + + static string GetBodyPartQuery (string partSpec, bool headersOnly, out string[] tags) + { + string query; + + if (headersOnly) { + tags = new string[1]; + + if (partSpec.Length > 0) { + query = string.Format ("BODY.PEEK[{0}.MIME]", partSpec); + tags[0] = partSpec + ".MIME"; + } else { + query = "BODY.PEEK[HEADER]"; + tags[0] = "HEADER"; + } + } else if (partSpec.Length > 0) { + tags = new string[] { + partSpec + ".MIME", + partSpec + }; + + query = string.Format ("BODY.PEEK[{0}] BODY.PEEK[{1}]", tags[0], tags[1]); + } else { + tags = new string[] { string.Empty }; + query = "BODY.PEEK[]"; + } + + return query; + } + + ImapCommand QueueGetHeadersCommand (UniqueId uid, CancellationToken cancellationToken, ITransferProgress? progress, out FetchStreamContext ctx) + { + if (!uid.IsValid) + throw new ArgumentException ("The uid is invalid.", nameof (uid)); + + CheckState (true, false); + + var ic = new ImapCommand (Engine, cancellationToken, this, "UID FETCH %u (BODY.PEEK[HEADER])\r\n", uid.Id); + ic.RegisterUntaggedHandler ("FETCH", FetchStreamHandler); + ic.UserData = ctx = new FetchStreamContext (progress); + + Engine.QueueCommand (ic); + + return ic; + } + + Stream ProcessGetHeadersResponse (ImapCommand ic, FetchStreamContext ctx, UniqueId uid) + { + ProcessFetchResponse (ic); + + if (!ctx.TryGetSection (uid, "HEADER", out var section, true)) + throw new MessageNotFoundException ("The IMAP server did not return the requested message headers."); + + return section.Stream; + } + + /// + /// Get the specified message headers. + /// + /// + /// Gets the specified message headers. + /// + /// The message headers. + /// The UID of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message headers. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override HeaderList GetHeaders (UniqueId uid, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + var ic = QueueGetHeadersCommand (uid, cancellationToken, progress, out var ctx); + + try { + Engine.Run (ic); + + var stream = ProcessGetHeadersResponse (ic, ctx, uid); + + return ParseHeaders (stream, cancellationToken); + } finally { + ctx.Dispose (); + } + } + + /// + /// Asynchronously get the specified message headers. + /// + /// + /// Gets the specified message headers. + /// + /// The message headers. + /// The UID of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message headers. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override async Task GetHeadersAsync (UniqueId uid, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + var ic = QueueGetHeadersCommand (uid, cancellationToken, progress, out var ctx); + + try { + await Engine.RunAsync (ic).ConfigureAwait (false); + + var stream = ProcessGetHeadersResponse (ic, ctx, uid); + + return await ParseHeadersAsync (stream, cancellationToken).ConfigureAwait (false); + } finally { + ctx.Dispose (); + } + } + + ImapCommand QueueGetHeadersCommand (UniqueId uid, string partSpecifier, CancellationToken cancellationToken, ITransferProgress? progress, out FetchStreamContext ctx, out string[] tags) + { + if (!uid.IsValid) + throw new ArgumentException ("The uid is invalid.", nameof (uid)); + + if (partSpecifier == null) + throw new ArgumentNullException (nameof (partSpecifier)); + + CheckState (true, false); + + var command = string.Format ("UID FETCH {0} ({1})\r\n", uid, GetBodyPartQuery (partSpecifier, true, out tags)); + var ic = new ImapCommand (Engine, cancellationToken, this, command); + ic.RegisterUntaggedHandler ("FETCH", FetchStreamHandler); + ic.UserData = ctx = new FetchStreamContext (progress); + + Engine.QueueCommand (ic); + + return ic; + } + + Stream ProcessGetHeadersResponse (ImapCommand ic, FetchStreamContext ctx, UniqueId uid, string[] tags) + { + ProcessFetchResponse (ic); + + if (!ctx.TryGetSection (uid, tags[0], out var section, true)) + throw new MessageNotFoundException ("The IMAP server did not return the requested body part headers."); + + return section.Stream; + } + + /// + /// Get the specified body part headers. + /// + /// + /// Gets the specified body part headers. + /// + /// The body part headers. + /// The UID of the message. + /// The body part specifier. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested body part headers. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public virtual HeaderList GetHeaders (UniqueId uid, string partSpecifier, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + var ic = QueueGetHeadersCommand (uid, partSpecifier, cancellationToken, progress, out var ctx, out var tags); + + try { + Engine.Run (ic); + + var stream = ProcessGetHeadersResponse (ic, ctx, uid, tags); + + return ParseHeaders (stream, cancellationToken); + } finally { + ctx.Dispose (); + } + } + + /// + /// Asynchronously get the specified body part headers. + /// + /// + /// Gets the specified body part headers. + /// + /// The body part headers. + /// The UID of the message. + /// The body part specifier. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested body part headers. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public virtual async Task GetHeadersAsync (UniqueId uid, string partSpecifier, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + var ic = QueueGetHeadersCommand (uid, partSpecifier, cancellationToken, progress, out var ctx, out var tags); + + try { + await Engine.RunAsync (ic).ConfigureAwait (false); + + var stream = ProcessGetHeadersResponse (ic, ctx, uid, tags); + + return await ParseHeadersAsync (stream, cancellationToken).ConfigureAwait (false); + } finally { + ctx.Dispose (); + } + } + + /// + /// Get the specified body part headers. + /// + /// + /// Gets the specified body part headers. + /// + /// The body part headers. + /// The UID of the message. + /// The body part. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested body part headers. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override HeaderList GetHeaders (UniqueId uid, BodyPart part, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + if (!uid.IsValid) + throw new ArgumentException ("The uid is invalid.", nameof (uid)); + + if (part == null) + throw new ArgumentNullException (nameof (part)); + + return GetHeaders (uid, part.PartSpecifier, cancellationToken, progress); + } + + /// + /// Asynchronously get the specified body part headers. + /// + /// + /// Gets the specified body part headers. + /// + /// The body part headers. + /// The UID of the message. + /// The body part. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested body part headers. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task GetHeadersAsync (UniqueId uid, BodyPart part, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + if (!uid.IsValid) + throw new ArgumentException ("The uid is invalid.", nameof (uid)); + + if (part == null) + throw new ArgumentNullException (nameof (part)); + + return GetHeadersAsync (uid, part.PartSpecifier, cancellationToken, progress); + } + + ImapCommand QueueGetHeadersCommand (int index, CancellationToken cancellationToken, ITransferProgress? progress, out FetchStreamContext ctx) + { + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException (nameof (index)); + + CheckState (true, false); + + var ic = new ImapCommand (Engine, cancellationToken, this, "FETCH %d (BODY.PEEK[HEADER])\r\n", index + 1); + ic.RegisterUntaggedHandler ("FETCH", FetchStreamHandler); + ic.UserData = ctx = new FetchStreamContext (progress); + + Engine.QueueCommand (ic); + + return ic; + } + + Stream ProcessGetHeadersResponse (ImapCommand ic, FetchStreamContext ctx, int index) + { + ProcessFetchResponse (ic); + + if (!ctx.TryGetSection (index, "HEADER", out var section, true)) + throw new MessageNotFoundException ("The IMAP server did not return the requested message headers."); + + return section.Stream; + } + + /// + /// Get the specified message headers. + /// + /// + /// Gets the specified message headers. + /// + /// The message headers. + /// The index of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message headers. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override HeaderList GetHeaders (int index, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + var ic = QueueGetHeadersCommand (index, cancellationToken, progress, out var ctx); + + try { + Engine.Run (ic); + + var stream = ProcessGetHeadersResponse (ic, ctx, index); + + return ParseHeaders (stream, cancellationToken); + } finally { + ctx.Dispose (); + } + } + + /// + /// Asynchronously get the specified message headers. + /// + /// + /// Gets the specified message headers. + /// + /// The message headers. + /// The index of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message headers. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override async Task GetHeadersAsync (int index, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + var ic = QueueGetHeadersCommand (index, cancellationToken, progress, out var ctx); + + try { + await Engine.RunAsync (ic).ConfigureAwait (false); + + var stream = ProcessGetHeadersResponse (ic, ctx, index); + + return await ParseHeadersAsync (stream, cancellationToken).ConfigureAwait (false); + } finally { + ctx.Dispose (); + } + } + + ImapCommand QueueGetHeadersCommand (int index, string partSpecifier, CancellationToken cancellationToken, ITransferProgress? progress, out FetchStreamContext ctx, out string[] tags) + { + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException (nameof (index)); + + if (partSpecifier == null) + throw new ArgumentNullException (nameof (partSpecifier)); + + CheckState (true, false); + + var command = string.Format ("FETCH {0} ({1})\r\n", index + 1, GetBodyPartQuery (partSpecifier, true, out tags)); + var ic = new ImapCommand (Engine, cancellationToken, this, command); + ic.RegisterUntaggedHandler ("FETCH", FetchStreamHandler); + ic.UserData = ctx = new FetchStreamContext (progress); + + Engine.QueueCommand (ic); + + return ic; + } + + Stream ProcessGetHeadersResponse (ImapCommand ic, FetchStreamContext ctx, int index, string[] tags) + { + ProcessFetchResponse (ic); + + if (!ctx.TryGetSection (index, tags[0], out var section, true)) + throw new MessageNotFoundException ("The IMAP server did not return the requested body part headers."); + + return section.Stream; + } + + /// + /// Get the specified body part headers. + /// + /// + /// Gets the specified body part headers. + /// + /// The body part headers. + /// The index of the message. + /// The body part specifier. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is out of range. + /// + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested body part headers. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public virtual HeaderList GetHeaders (int index, string partSpecifier, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + var ic = QueueGetHeadersCommand (index, partSpecifier, cancellationToken, progress, out var ctx, out var tags); + + try { + Engine.Run (ic); + + var stream = ProcessGetHeadersResponse (ic, ctx, index, tags); + + return ParseHeaders (stream, cancellationToken); + } finally { + ctx.Dispose (); + } + } + + /// + /// Asynchronously get the specified body part headers. + /// + /// + /// Gets the specified body part headers. + /// + /// The body part headers. + /// The index of the message. + /// The body part specifier. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is out of range. + /// + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested body part headers. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public virtual async Task GetHeadersAsync (int index, string partSpecifier, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + var ic = QueueGetHeadersCommand (index, partSpecifier, cancellationToken, progress, out var ctx, out var tags); + + try { + await Engine.RunAsync (ic).ConfigureAwait (false); + + var stream = ProcessGetHeadersResponse (ic, ctx, index, tags); + + return await ParseHeadersAsync (stream, cancellationToken).ConfigureAwait (false); + } finally { + ctx.Dispose (); + } + } + + /// + /// Get the specified body part headers. + /// + /// + /// Gets the specified body part headers. + /// + /// The body part headers. + /// The index of the message. + /// The body part. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is out of range. + /// + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested body part headers. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override HeaderList GetHeaders (int index, BodyPart part, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException (nameof (index)); + + if (part == null) + throw new ArgumentNullException (nameof (part)); + + return GetHeaders (index, part.PartSpecifier, cancellationToken, progress); + } + + /// + /// Asynchronously get the specified body part headers. + /// + /// + /// Gets the specified body part headers. + /// + /// The body part headers. + /// The index of the message. + /// The body part. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is out of range. + /// + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested body part headers. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task GetHeadersAsync (int index, BodyPart part, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException (nameof (index)); + + if (part == null) + throw new ArgumentNullException (nameof (part)); + + return GetHeadersAsync (index, part.PartSpecifier, cancellationToken, progress); + } + + ImapCommand QueueGetMessageCommand (UniqueId uid, CancellationToken cancellationToken, ITransferProgress? progress, out FetchStreamContext ctx) + { + if (!uid.IsValid) + throw new ArgumentException ("The uid is invalid.", nameof (uid)); + + CheckState (true, false); + + var ic = new ImapCommand (Engine, cancellationToken, this, "UID FETCH %u (BODY.PEEK[])\r\n", uid.Id); + ic.RegisterUntaggedHandler ("FETCH", FetchStreamHandler); + ic.UserData = ctx = new FetchStreamContext (progress); + + Engine.QueueCommand (ic); + + return ic; + } + + Stream ProcessGetMessageResponse (ImapCommand ic, FetchStreamContext ctx, UniqueId uid) + { + ProcessFetchResponse (ic); + + if (!ctx.TryGetSection (uid, string.Empty, out var section, true)) + throw new MessageNotFoundException ("The IMAP server did not return the requested message."); + + return section.Stream; + } + + /// + /// Get the specified message. + /// + /// + /// Gets the specified message. + /// + /// The message. + /// The UID of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override MimeMessage GetMessage (UniqueId uid, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + var ic = QueueGetMessageCommand (uid, cancellationToken, progress, out var ctx); + + try { + Engine.Run (ic); + + var stream = ProcessGetMessageResponse (ic, ctx, uid); + + return ParseMessage (stream, cancellationToken); + } finally { + ctx.Dispose (); + } + } + + /// + /// Asynchronously get the specified message. + /// + /// + /// Gets the specified message. + /// + /// The message. + /// The UID of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override async Task GetMessageAsync (UniqueId uid, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + var ic = QueueGetMessageCommand (uid, cancellationToken, progress, out var ctx); + + try { + await Engine.RunAsync (ic).ConfigureAwait (false); + + var stream = ProcessGetMessageResponse (ic, ctx, uid); + + return await ParseMessageAsync (stream, cancellationToken).ConfigureAwait (false); + } finally { + ctx.Dispose (); + } + } + + ImapCommand QueueGetMessageCommand (int index, CancellationToken cancellationToken, ITransferProgress? progress, out FetchStreamContext ctx) + { + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException (nameof (index)); + + CheckState (true, false); + + var ic = new ImapCommand (Engine, cancellationToken, this, "FETCH %d (BODY.PEEK[])\r\n", index + 1); + ic.RegisterUntaggedHandler ("FETCH", FetchStreamHandler); + ic.UserData = ctx = new FetchStreamContext (progress); + + Engine.QueueCommand (ic); + + return ic; + } + + Stream ProcessGetMessageResponse (ImapCommand ic, FetchStreamContext ctx, int index) + { + ProcessFetchResponse (ic); + + if (!ctx.TryGetSection (index, string.Empty, out var section, true)) + throw new MessageNotFoundException ("The IMAP server did not return the requested message."); + + return section.Stream; + } + + /// + /// Get the specified message. + /// + /// + /// Gets the specified message. + /// + /// The message. + /// The index of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override MimeMessage GetMessage (int index, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + var ic = QueueGetMessageCommand (index, cancellationToken, progress, out var ctx); + + try { + Engine.Run (ic); + + var stream = ProcessGetMessageResponse (ic, ctx, index); + + return ParseMessage (stream, cancellationToken); + } finally { + ctx.Dispose (); + } + } + + /// + /// Asynchronously get the specified message. + /// + /// + /// Gets the specified message. + /// + /// The message. + /// The index of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override async Task GetMessageAsync (int index, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + var ic = QueueGetMessageCommand (index, cancellationToken, progress, out var ctx); + + try { + await Engine.RunAsync (ic).ConfigureAwait (false); + + var stream = ProcessGetMessageResponse (ic, ctx, index); + + return await ParseMessageAsync (stream, cancellationToken).ConfigureAwait (false); + } finally { + ctx.Dispose (); + } + } + + ImapCommand QueueGetBodyPartCommand (UniqueId uid, string partSpecifier, CancellationToken cancellationToken, ITransferProgress? progress, out FetchStreamContext ctx, out string[] tags) + { + if (!uid.IsValid) + throw new ArgumentException ("The uid is invalid.", nameof (uid)); + + if (partSpecifier == null) + throw new ArgumentNullException (nameof (partSpecifier)); + + CheckState (true, false); + + var command = string.Format ("UID FETCH {0} ({1})\r\n", uid, GetBodyPartQuery (partSpecifier, false, out tags)); + var ic = new ImapCommand (Engine, cancellationToken, this, command); + ic.RegisterUntaggedHandler ("FETCH", FetchStreamHandler); + ic.UserData = ctx = new FetchStreamContext (progress); + + Engine.QueueCommand (ic); + + return ic; + } + + void ProcessGetBodyPartResponse (ImapCommand ic, FetchStreamContext ctx, UniqueId uid, string[] tags, out ChainedStream chained, out bool dispose) + { + ProcessFetchResponse (ic); + + chained = new ChainedStream (); + dispose = false; + + try { + foreach (var tag in tags) { + if (!ctx.TryGetSection (uid, tag, out var section, true)) + throw new MessageNotFoundException ("The IMAP server did not return the requested body part."); + + if (!(section.Stream is MemoryStream || section.Stream is MemoryBlockStream)) + dispose = true; + + chained.Add (section.Stream); + } + } catch { + chained.Dispose (); + throw; + } + } + + void RemoveMessageHeaders (MimeEntity entity) + { + for (int i = entity.Headers.Count; i > 0; i--) { + var header = entity.Headers[i - 1]; + + if (!header.Field.StartsWith ("Content-", StringComparison.OrdinalIgnoreCase)) + entity.Headers.RemoveAt (i - 1); + } + } + + /// + /// Get the specified body part. + /// + /// + /// Gets the specified body part. + /// For more information about how to construct the , + /// see RFC3501, Section 6.4.5. + /// + /// + /// + /// + /// The body part. + /// The UID of the message. + /// The body part specifier. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message body. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public virtual MimeEntity GetBodyPart (UniqueId uid, string partSpecifier, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + var ic = QueueGetBodyPartCommand (uid, partSpecifier, cancellationToken, progress, out var ctx, out var tags); + ChainedStream chained; + bool dispose; + + try { + Engine.Run (ic); + + ProcessGetBodyPartResponse (ic, ctx, uid, tags, out chained, out dispose); + } finally { + ctx.Dispose (); + } + + var entity = ParseEntity (chained, dispose, cancellationToken); + + if (partSpecifier.Length == 0) + RemoveMessageHeaders (entity); + + return entity; + } + + /// + /// Asynchronously get the specified body part. + /// + /// + /// Gets the specified body part. + /// For more information about how to construct the , + /// see RFC3501, Section 6.4.5. + /// + /// + /// + /// + /// The body part. + /// The UID of the message. + /// The body part specifier. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message body. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public virtual async Task GetBodyPartAsync (UniqueId uid, string partSpecifier, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + var ic = QueueGetBodyPartCommand (uid, partSpecifier, cancellationToken, progress, out var ctx, out var tags); + ChainedStream chained; + bool dispose; + + try { + await Engine.RunAsync (ic).ConfigureAwait (false); + + ProcessGetBodyPartResponse (ic, ctx, uid, tags, out chained, out dispose); + } finally { + ctx.Dispose (); + } + + var entity = await ParseEntityAsync (chained, dispose, cancellationToken).ConfigureAwait (false); + + if (partSpecifier.Length == 0) + RemoveMessageHeaders (entity); + + return entity; + } + + /// + /// Get the specified body part. + /// + /// + /// Gets the specified body part. + /// + /// + /// + /// + /// The body part. + /// The UID of the message. + /// The body part. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message body. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override MimeEntity GetBodyPart (UniqueId uid, BodyPart part, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + if (!uid.IsValid) + throw new ArgumentException ("The uid is invalid.", nameof (uid)); + + if (part == null) + throw new ArgumentNullException (nameof (part)); + + return GetBodyPart (uid, part.PartSpecifier, cancellationToken, progress); + } + + /// + /// Asynchronously get the specified body part. + /// + /// + /// Gets the specified body part. + /// + /// + /// + /// + /// The body part. + /// The UID of the message. + /// The body part. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message body. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task GetBodyPartAsync (UniqueId uid, BodyPart part, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + if (!uid.IsValid) + throw new ArgumentException ("The uid is invalid.", nameof (uid)); + + if (part == null) + throw new ArgumentNullException (nameof (part)); + + return GetBodyPartAsync (uid, part.PartSpecifier, cancellationToken, progress); + } + + ImapCommand QueueGetBodyPartCommand (int index, string partSpecifier, CancellationToken cancellationToken, ITransferProgress? progress, out FetchStreamContext ctx, out string[] tags) + { + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException (nameof (index)); + + if (partSpecifier == null) + throw new ArgumentNullException (nameof (partSpecifier)); + + CheckState (true, false); + + var seqid = (index + 1).ToString (CultureInfo.InvariantCulture); + var command = string.Format ("FETCH {0} ({1})\r\n", seqid, GetBodyPartQuery (partSpecifier, false, out tags)); + var ic = new ImapCommand (Engine, cancellationToken, this, command); + ic.RegisterUntaggedHandler ("FETCH", FetchStreamHandler); + ic.UserData = ctx = new FetchStreamContext (progress); + + Engine.QueueCommand (ic); + + return ic; + } + + void ProcessGetBodyPartResponse (ImapCommand ic, FetchStreamContext ctx, int index, string[] tags, out ChainedStream chained, out bool dispose) + { + ProcessFetchResponse (ic); + + chained = new ChainedStream (); + dispose = false; + + try { + foreach (var tag in tags) { + if (!ctx.TryGetSection (index, tag, out var section, true)) + throw new MessageNotFoundException ("The IMAP server did not return the requested body part."); + + if (!(section.Stream is MemoryStream || section.Stream is MemoryBlockStream)) + dispose = true; + + chained.Add (section.Stream); + } + } catch { + chained.Dispose (); + throw; + } + } + + /// + /// Get the specified body part. + /// + /// + /// Gets the specified body part. + /// For more information about how to construct the , + /// see RFC3501, Section 6.4.5. + /// + /// The body part. + /// The index of the message. + /// The body part specifier. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public virtual MimeEntity GetBodyPart (int index, string partSpecifier, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + var ic = QueueGetBodyPartCommand (index, partSpecifier, cancellationToken, progress, out var ctx, out var tags); + ChainedStream chained; + bool dispose; + + try { + Engine.Run (ic); + + ProcessGetBodyPartResponse (ic, ctx, index, tags, out chained, out dispose); + } finally { + ctx.Dispose (); + } + + var entity = ParseEntity (chained, dispose, cancellationToken); + + if (partSpecifier.Length == 0) + RemoveMessageHeaders (entity); + + return entity; + } + + /// + /// Asynchronously get the specified body part. + /// + /// + /// Gets the specified body part. + /// For more information about how to construct the , + /// see RFC3501, Section 6.4.5. + /// + /// The body part. + /// The index of the message. + /// The body part specifier. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public virtual async Task GetBodyPartAsync (int index, string partSpecifier, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + var ic = QueueGetBodyPartCommand (index, partSpecifier, cancellationToken, progress, out var ctx, out var tags); + ChainedStream chained; + bool dispose; + + try { + await Engine.RunAsync (ic).ConfigureAwait (false); + + ProcessGetBodyPartResponse (ic, ctx, index, tags, out chained, out dispose); + } finally { + ctx.Dispose (); + } + + var entity = await ParseEntityAsync (chained, dispose, cancellationToken).ConfigureAwait (false); + + if (partSpecifier.Length == 0) + RemoveMessageHeaders (entity); + + return entity; + } + + /// + /// Get the specified body part. + /// + /// + /// Gets the specified body part. + /// + /// The body part. + /// The index of the message. + /// The body part. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override MimeEntity GetBodyPart (int index, BodyPart part, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException (nameof (index)); + + if (part == null) + throw new ArgumentNullException (nameof (part)); + + return GetBodyPart (index, part.PartSpecifier, cancellationToken, progress); + } + + /// + /// Asynchronously get the specified body part. + /// + /// + /// Gets the specified body part. + /// + /// The body part. + /// The index of the message. + /// The body part. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task GetBodyPartAsync (int index, BodyPart part, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException (nameof (index)); + + if (part == null) + throw new ArgumentNullException (nameof (part)); + + return GetBodyPartAsync (index, part.PartSpecifier, cancellationToken, progress); + } + + bool TryQueueGetStreamCommand (UniqueId uid, int offset, int count, CancellationToken cancellationToken, ITransferProgress? progress, [NotNullWhen (true)] out ImapCommand? ic, [NotNullWhen (true)] out FetchStreamContext? ctx) + { + if (!uid.IsValid) + throw new ArgumentException ("The uid is invalid.", nameof (uid)); + + if (offset < 0) + throw new ArgumentOutOfRangeException (nameof (offset)); + + if (count < 0) + throw new ArgumentOutOfRangeException (nameof (count)); + + CheckState (true, false); + + if (count == 0) { + ctx = null; + ic = null; + return false; + } + + ic = new ImapCommand (Engine, cancellationToken, this, "UID FETCH %u (BODY.PEEK[]<%d.%d>)\r\n", uid.Id, offset, count); + ic.RegisterUntaggedHandler ("FETCH", FetchStreamHandler); + ic.UserData = ctx = new FetchStreamContext (progress); + + Engine.QueueCommand (ic); + + return true; + } + + Stream ProcessGetStreamResponse (ImapCommand ic, FetchStreamContext ctx, UniqueId uid) + { + ProcessFetchResponse (ic); + + if (!ctx.TryGetSection (uid, string.Empty, out var section, true)) + throw new MessageNotFoundException ("The IMAP server did not return the requested stream."); + + return section.Stream; + } + + /// + /// Get a substream of the specified message. + /// + /// + /// Fetches a substream of the message. If the starting offset is beyond + /// the end of the message, an empty stream is returned. If the number of + /// bytes desired extends beyond the end of the message, the stream will + /// end where the message ends. + /// + /// The stream. + /// The UID of the message. + /// The starting offset of the first desired byte. + /// The number of bytes desired. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is invalid. + /// + /// + /// is negative. + /// -or- + /// is negative. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Stream GetStream (UniqueId uid, int offset, int count, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + if (!TryQueueGetStreamCommand (uid, offset, count, cancellationToken, progress, out var ic, out var ctx)) + return new MemoryStream (); + + try { + Engine.Run (ic); + + return ProcessGetStreamResponse (ic, ctx, uid); + } finally { + ctx.Dispose (); + } + } + + /// + /// Asynchronously gets a substream of the specified message. + /// + /// + /// Fetches a substream of the message. If the starting offset is beyond + /// the end of the message, an empty stream is returned. If the number of + /// bytes desired extends beyond the end of the message, the stream will + /// end where the message ends. + /// + /// The stream. + /// The UID of the message. + /// The starting offset of the first desired byte. + /// The number of bytes desired. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is invalid. + /// + /// + /// is negative. + /// -or- + /// is negative. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override async Task GetStreamAsync (UniqueId uid, int offset, int count, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + if (!TryQueueGetStreamCommand (uid, offset, count, cancellationToken, progress, out var ic, out var ctx)) + return new MemoryStream (); + + try { + await Engine.RunAsync (ic).ConfigureAwait (false); + + return ProcessGetStreamResponse (ic, ctx, uid); + } finally { + ctx.Dispose (); + } + } + + bool TryQueueGetStreamCommand (int index, int offset, int count, CancellationToken cancellationToken, ITransferProgress? progress, [NotNullWhen (true)] out ImapCommand? ic, [NotNullWhen (true)] out FetchStreamContext? ctx) + { + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException (nameof (index)); + + if (offset < 0) + throw new ArgumentOutOfRangeException (nameof (offset)); + + if (count < 0) + throw new ArgumentOutOfRangeException (nameof (count)); + + CheckState (true, false); + + if (count == 0) { + ctx = null; + ic = null; + return false; + } + + ic = new ImapCommand (Engine, cancellationToken, this, "FETCH %d (BODY.PEEK[]<%d.%d>)\r\n", index + 1, offset, count); + ic.RegisterUntaggedHandler ("FETCH", FetchStreamHandler); + ic.UserData = ctx = new FetchStreamContext (progress); + + Engine.QueueCommand (ic); + + return true; + } + + Stream ProcessGetStreamResponse (ImapCommand ic, FetchStreamContext ctx, int index) + { + ProcessFetchResponse (ic); + + if (!ctx.TryGetSection (index, string.Empty, out var section, true)) + throw new MessageNotFoundException ("The IMAP server did not return the requested stream."); + + return section.Stream; + } + + /// + /// Get a substream of the specified message. + /// + /// + /// Fetches a substream of the message. If the starting offset is beyond + /// the end of the message, an empty stream is returned. If the number of + /// bytes desired extends beyond the end of the message, a truncated stream + /// will be returned. + /// + /// The stream. + /// The index of the message. + /// The starting offset of the first desired byte. + /// The number of bytes desired. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is out of range. + /// -or- + /// is negative. + /// -or- + /// is negative. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Stream GetStream (int index, int offset, int count, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + if (!TryQueueGetStreamCommand (index, offset, count, cancellationToken, progress, out var ic, out var ctx)) + return new MemoryStream (); + + try { + Engine.Run (ic); + + return ProcessGetStreamResponse (ic, ctx, index); + } finally { + ctx.Dispose (); + } + } + + /// + /// Asynchronously gets a substream of the specified message. + /// + /// + /// Fetches a substream of the message. If the starting offset is beyond + /// the end of the message, an empty stream is returned. If the number of + /// bytes desired extends beyond the end of the message, a truncated stream + /// will be returned. + /// + /// The stream. + /// The index of the message. + /// The starting offset of the first desired byte. + /// The number of bytes desired. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is out of range. + /// -or- + /// is negative. + /// -or- + /// is negative. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override async Task GetStreamAsync (int index, int offset, int count, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + if (!TryQueueGetStreamCommand (index, offset, count, cancellationToken, progress, out var ic, out var ctx)) + return new MemoryStream (); + + try { + await Engine.RunAsync (ic).ConfigureAwait (false); + + return ProcessGetStreamResponse (ic, ctx, index); + } finally { + ctx.Dispose (); + } + } + + ImapCommand QueueGetStreamCommand (UniqueId uid, string section, CancellationToken cancellationToken, ITransferProgress? progress, out FetchStreamContext ctx) + { + if (!uid.IsValid) + throw new ArgumentException ("The uid is invalid.", nameof (uid)); + + if (section == null) + throw new ArgumentNullException (nameof (section)); + + CheckState (true, false); + + var command = string.Format ("UID FETCH {0} (BODY.PEEK[{1}])\r\n", uid, section); + var ic = new ImapCommand (Engine, cancellationToken, this, command); + ic.RegisterUntaggedHandler ("FETCH", FetchStreamHandler); + ic.UserData = ctx = new FetchStreamContext (progress); + + Engine.QueueCommand (ic); + + return ic; + } + + Stream ProcessGetStreamResponse (ImapCommand ic, FetchStreamContext ctx, UniqueId uid, string section) + { + ProcessFetchResponse (ic); + + if (!ctx.TryGetSection (uid, section, out var s, true)) + throw new MessageNotFoundException ("The IMAP server did not return the requested stream."); + + return s.Stream; + } + + /// + /// Get a substream of the specified body part. + /// + /// + /// Gets a substream of the specified message. + /// For more information about how to construct the , + /// see RFC3501, Section 6.4.5. + /// + /// + /// + /// + /// The stream. + /// The UID of the message. + /// The desired section of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is invalid. + /// + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Stream GetStream (UniqueId uid, string section, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + var ic = QueueGetStreamCommand (uid, section, cancellationToken, progress, out var ctx); + + try { + Engine.Run (ic); + + return ProcessGetStreamResponse (ic, ctx, uid, section); + } finally { + ctx.Dispose (); + } + } + + /// + /// Asynchronously gets a substream of the specified body part. + /// + /// + /// Gets a substream of the specified message. + /// For more information about how to construct the , + /// see RFC3501, Section 6.4.5. + /// + /// + /// + /// + /// The stream. + /// The UID of the message. + /// The desired section of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is invalid. + /// + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override async Task GetStreamAsync (UniqueId uid, string section, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + var ic = QueueGetStreamCommand (uid, section, cancellationToken, progress, out var ctx); + + try { + await Engine.RunAsync (ic).ConfigureAwait (false); + + return ProcessGetStreamResponse (ic, ctx, uid, section); + } finally { + ctx.Dispose (); + } + } + + bool TryQueueGetStreamCommand (UniqueId uid, string section, int offset, int count, CancellationToken cancellationToken, ITransferProgress? progress, [NotNullWhen (true)] out ImapCommand? ic, [NotNullWhen (true)] out FetchStreamContext? ctx) + { + if (!uid.IsValid) + throw new ArgumentException ("The uid is invalid.", nameof (uid)); + + if (section == null) + throw new ArgumentNullException (nameof (section)); + + if (offset < 0) + throw new ArgumentOutOfRangeException (nameof (offset)); + + if (count < 0) + throw new ArgumentOutOfRangeException (nameof (count)); + + CheckState (true, false); + + if (count == 0) { + ctx = null; + ic = null; + return false; + } + + var range = string.Format (CultureInfo.InvariantCulture, "{0}.{1}", offset, count); + var command = string.Format ("UID FETCH {0} (BODY.PEEK[{1}]<{2}>)\r\n", uid, section, range); + + ic = new ImapCommand (Engine, cancellationToken, this, command); + ic.RegisterUntaggedHandler ("FETCH", FetchStreamHandler); + ic.UserData = ctx = new FetchStreamContext (progress); + + Engine.QueueCommand (ic); + + return true; + } + + /// + /// Get a substream of the specified message. + /// + /// + /// Gets a substream of the specified message. If the starting offset is beyond + /// the end of the specified section of the message, an empty stream is returned. If + /// the number of bytes desired extends beyond the end of the section, a truncated + /// stream will be returned. + /// For more information about how to construct the , + /// see RFC3501, Section 6.4.5. + /// + /// The stream. + /// The UID of the message. + /// The desired section of the message. + /// The starting offset of the first desired byte. + /// The number of bytes desired. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is invalid. + /// + /// + /// is . + /// + /// + /// is negative. + /// -or- + /// is negative. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Stream GetStream (UniqueId uid, string section, int offset, int count, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + if (!TryQueueGetStreamCommand (uid, section, offset, count, cancellationToken, progress, out var ic, out var ctx)) + return new MemoryStream (); + + try { + Engine.Run (ic); + + return ProcessGetStreamResponse (ic, ctx, uid, section); + } finally { + ctx.Dispose (); + } + } + + /// + /// Asynchronously gets a substream of the specified message. + /// + /// + /// Gets a substream of the specified message. If the starting offset is beyond + /// the end of the specified section of the message, an empty stream is returned. If + /// the number of bytes desired extends beyond the end of the section, a truncated + /// stream will be returned. + /// For more information about how to construct the , + /// see RFC3501, Section 6.4.5. + /// + /// The stream. + /// The UID of the message. + /// The desired section of the message. + /// The starting offset of the first desired byte. + /// The number of bytes desired. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is invalid. + /// + /// + /// is . + /// + /// + /// is negative. + /// -or- + /// is negative. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override async Task GetStreamAsync (UniqueId uid, string section, int offset, int count, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + if (!TryQueueGetStreamCommand (uid, section, offset, count, cancellationToken, progress, out var ic, out var ctx)) + return new MemoryStream (); + + try { + await Engine.RunAsync (ic).ConfigureAwait (false); + + return ProcessGetStreamResponse (ic, ctx, uid, section); + } finally { + ctx.Dispose (); + } + } + + ImapCommand QueueGetStreamCommand (int index, string section, CancellationToken cancellationToken, ITransferProgress? progress, out FetchStreamContext ctx) + { + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException (nameof (index)); + + if (section == null) + throw new ArgumentNullException (nameof (section)); + + CheckState (true, false); + + var seqid = (index + 1).ToString (CultureInfo.InvariantCulture); + var command = string.Format ("FETCH {0} (BODY.PEEK[{1}])\r\n", seqid, section); + var ic = new ImapCommand (Engine, cancellationToken, this, command); + ic.RegisterUntaggedHandler ("FETCH", FetchStreamHandler); + ic.UserData = ctx = new FetchStreamContext (progress); + + Engine.QueueCommand (ic); + + return ic; + } + + Stream ProcessGetStreamResponse (ImapCommand ic, FetchStreamContext ctx, int index, string section) + { + ProcessFetchResponse (ic); + + if (!ctx.TryGetSection (index, section, out var sect, true)) + throw new MessageNotFoundException ("The IMAP server did not return the requested stream."); + + return sect.Stream; + } + + /// + /// Get a substream of the specified message. + /// + /// + /// Gets a substream of the specified message. + /// For more information about how to construct the , + /// see RFC3501, Section 6.4.5. + /// + /// The stream. + /// The index of the message. + /// The desired section of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Stream GetStream (int index, string section, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + var ic = QueueGetStreamCommand (index, section, cancellationToken, progress, out var ctx); + + try { + Engine.Run (ic); + + return ProcessGetStreamResponse (ic, ctx, index, section); + } finally { + ctx.Dispose (); + } + } + + /// + /// Asynchronously gets a substream of the specified message. + /// + /// + /// Gets a substream of the specified message. + /// For more information about how to construct the , + /// see RFC3501, Section 6.4.5. + /// + /// The stream. + /// The index of the message. + /// The desired section of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override async Task GetStreamAsync (int index, string section, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + var ic = QueueGetStreamCommand (index, section, cancellationToken, progress, out var ctx); + + try { + await Engine.RunAsync (ic).ConfigureAwait (false); + + return ProcessGetStreamResponse (ic, ctx, index, section); + } finally { + ctx.Dispose (); + } + } + + bool TryQueueGetStreamCommand (int index, string section, int offset, int count, CancellationToken cancellationToken, ITransferProgress? progress, [NotNullWhen (true)] out ImapCommand? ic, [NotNullWhen (true)] out FetchStreamContext? ctx) + { + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException (nameof (index)); + + if (section == null) + throw new ArgumentNullException (nameof (section)); + + if (offset < 0) + throw new ArgumentOutOfRangeException (nameof (offset)); + + if (count < 0) + throw new ArgumentOutOfRangeException (nameof (count)); + + CheckState (true, false); + + if (count == 0) { + ctx = null; + ic = null; + return false; + } + + var seqid = (index + 1).ToString (CultureInfo.InvariantCulture); + var range = string.Format (CultureInfo.InvariantCulture, "{0}.{1}", offset, count); + var command = string.Format ("FETCH {0} (BODY.PEEK[{1}]<{2}>)\r\n", seqid, section, range); + + ic = new ImapCommand (Engine, cancellationToken, this, command); + ic.RegisterUntaggedHandler ("FETCH", FetchStreamHandler); + ic.UserData = ctx = new FetchStreamContext (progress); + + Engine.QueueCommand (ic); + + return true; + } + + /// + /// Get a substream of the specified message. + /// + /// + /// Gets a substream of the specified message. If the starting offset is beyond + /// the end of the specified section of the message, an empty stream is returned. If + /// the number of bytes desired extends beyond the end of the section, a truncated + /// stream will be returned. + /// For more information about how to construct the , + /// see RFC3501, Section 6.4.5. + /// + /// The stream. + /// The index of the message. + /// The desired section of the message. + /// The starting offset of the first desired byte. + /// The number of bytes desired. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// + /// + /// is out of range. + /// -or- + /// is negative. + /// -or- + /// is negative. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Stream GetStream (int index, string section, int offset, int count, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + if (!TryQueueGetStreamCommand (index, section, offset, count, cancellationToken, progress, out var ic, out var ctx)) + return new MemoryStream (); + + try { + Engine.Run (ic); + + return ProcessGetStreamResponse (ic, ctx, index, section); + } finally { + ctx.Dispose (); + } + } + + /// + /// Asynchronously gets a substream of the specified message. + /// + /// + /// Gets a substream of the specified message. If the starting offset is beyond + /// the end of the specified section of the message, an empty stream is returned. If + /// the number of bytes desired extends beyond the end of the section, a truncated + /// stream will be returned. + /// For more information about how to construct the , + /// see RFC3501, Section 6.4.5. + /// + /// The stream. + /// The index of the message. + /// The desired section of the message. + /// The starting offset of the first desired byte. + /// The number of bytes desired. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// + /// + /// is out of range. + /// -or- + /// is negative. + /// -or- + /// is negative. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override async Task GetStreamAsync (int index, string section, int offset, int count, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + if (!TryQueueGetStreamCommand (index, section, offset, count, cancellationToken, progress, out var ic, out var ctx)) + return new MemoryStream (); + + try { + await Engine.RunAsync (ic).ConfigureAwait (false); + + return ProcessGetStreamResponse (ic, ctx, index, section); + } finally { + ctx.Dispose (); + } + } + + class FetchStreamCallbackContext : FetchStreamContextBase + { + readonly ImapFolder folder; + readonly object callback; + + public FetchStreamCallbackContext (ImapFolder folder, object callback, ITransferProgress? progress) : base (progress) + { + this.folder = folder; + this.callback = callback; + } + + void InvokeCallback (ImapFolder folder, int index, UniqueId uid, Stream stream, CancellationToken cancellationToken) + { + ((ImapFetchStreamCallback) callback) (folder, index, uid, stream); + } + + Task InvokeCallbackAsync (ImapFolder folder, int index, UniqueId uid, Stream stream, CancellationToken cancellationToken) + { + return ((ImapFetchStreamAsyncCallback) callback) (folder, index, uid, stream, cancellationToken); + } + + public override void Add (Section section, CancellationToken cancellationToken) + { + if (section.UniqueId.HasValue) { + InvokeCallback (folder, section.Index, section.UniqueId.Value, section.Stream, cancellationToken); + section.Stream.Dispose (); + } else { + Sections.Add (section); + } + } + + public override async Task AddAsync (Section section, CancellationToken cancellationToken) + { + if (section.UniqueId.HasValue) { + await InvokeCallbackAsync (folder, section.Index, section.UniqueId.Value, section.Stream, cancellationToken).ConfigureAwait (false); + section.Stream.Dispose (); + } else { + Sections.Add (section); + } + } + + public override void SetUniqueId (int index, UniqueId uid, CancellationToken cancellationToken) + { + for (int i = 0; i < Sections.Count; i++) { + if (Sections[i].Index == index) { + InvokeCallback (folder, index, uid, Sections[i].Stream, cancellationToken); + Sections[i].Stream.Dispose (); + Sections.RemoveAt (i); + break; + } + } + } + + public override async Task SetUniqueIdAsync (int index, UniqueId uid, CancellationToken cancellationToken) + { + for (int i = 0; i < Sections.Count; i++) { + if (Sections[i].Index == index) { + await InvokeCallbackAsync (folder, index, uid, Sections[i].Stream, cancellationToken).ConfigureAwait (false); + Sections[i].Stream.Dispose (); + Sections.RemoveAt (i); + break; + } + } + } + } + + void ValidateArguments (IList uids, object callback) + { + if (uids == null) + throw new ArgumentNullException (nameof (uids)); + + if (callback == null) + throw new ArgumentNullException (nameof (callback)); + + CheckState (true, false); + } + + IEnumerable QueueGetStreamsCommands (FetchStreamCallbackContext ctx, IList uids, object callback, CancellationToken cancellationToken) + { + foreach (var ic in Engine.CreateCommands (cancellationToken, this, "UID FETCH %s (BODY.PEEK[])\r\n", uids)) { + ic.RegisterUntaggedHandler ("FETCH", FetchStreamHandler); + ic.UserData = ctx; + + Engine.QueueCommand (ic); + + yield return ic; + } + } + + /// + /// Get the streams for the specified messages. + /// + /// + /// Gets the streams for the specified messages. + /// + /// The uids of the messages. + /// A callback method that gets called for each stream as it is received. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public virtual void GetStreams (IList uids, ImapFetchStreamCallback callback, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + ValidateArguments (uids, callback); + + if (uids.Count == 0) + return; + + using (var ctx = new FetchStreamCallbackContext (this, callback, progress)) { + foreach (var ic in QueueGetStreamsCommands (ctx, uids, callback, cancellationToken)) { + Engine.Run (ic); + + ProcessFetchResponse (ic); + } + } + } + + /// + /// Asynchronously get the streams for the specified messages. + /// + /// + /// Asynchronously gets the streams for the specified messages. + /// + /// An awaitable task. + /// The uids of the messages. + /// A callback method that gets called for each stream as it is received. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public virtual async Task GetStreamsAsync (IList uids, ImapFetchStreamAsyncCallback callback, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + ValidateArguments (uids, callback); + + if (uids.Count == 0) + return; + + using (var ctx = new FetchStreamCallbackContext (this, callback, progress)) { + foreach (var ic in QueueGetStreamsCommands (ctx, uids, callback, cancellationToken)) { + await Engine.RunAsync (ic).ConfigureAwait (false); + + ProcessFetchResponse (ic); + } + } + } + + void ValidateArguments (IList indexes, object callback) + { + if (indexes == null) + throw new ArgumentNullException (nameof (indexes)); + + if (callback == null) + throw new ArgumentNullException (nameof (callback)); + + CheckState (true, false); + CheckAllowIndexes (); + } + + ImapCommand QueueGetStreamsCommand (FetchStreamCallbackContext ctx, IList indexes, CancellationToken cancellationToken) + { + var command = new StringBuilder ("FETCH "); + ImapUtils.FormatIndexSet (Engine, command, indexes); + command.Append (" (UID BODY.PEEK[])\r\n"); + + var ic = new ImapCommand (Engine, cancellationToken, this, command.ToString ()); + ic.RegisterUntaggedHandler ("FETCH", FetchStreamHandler); + ic.UserData = ctx; + + Engine.QueueCommand (ic); + + return ic; + } + + /// + /// Get the streams for the specified messages. + /// + /// + /// Gets the streams for the specified messages. + /// + /// The indexes of the messages. + /// A callback method that gets called for each stream as it is received. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public virtual void GetStreams (IList indexes, ImapFetchStreamCallback callback, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + ValidateArguments (indexes, callback); + + if (indexes.Count == 0) + return; + + using (var ctx = new FetchStreamCallbackContext (this, callback, progress)) { + var ic = QueueGetStreamsCommand (ctx, indexes, cancellationToken); + + Engine.Run (ic); + + ProcessFetchResponse (ic); + } + } + + /// + /// Asynchronously get the streams for the specified messages. + /// + /// + /// Asynchronously gets the streams for the specified messages. + /// + /// An awaitable task. + /// The indexes of the messages. + /// A callback method that gets called for each stream as it is received. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public virtual async Task GetStreamsAsync (IList indexes, ImapFetchStreamAsyncCallback callback, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + ValidateArguments (indexes, callback); + + if (indexes.Count == 0) + return; + + using (var ctx = new FetchStreamCallbackContext (this, callback, progress)) { + var ic = QueueGetStreamsCommand (ctx, indexes, cancellationToken); + + await Engine.RunAsync (ic).ConfigureAwait (false); + + ProcessFetchResponse (ic); + } + } + + void ValidateArguments (int min, int max, object callback) + { + if (min < 0) + throw new ArgumentOutOfRangeException (nameof (min)); + + if (max != -1 && max < min) + throw new ArgumentOutOfRangeException (nameof (max)); + + if (callback == null) + throw new ArgumentNullException (nameof (callback)); + + CheckState (true, false); + CheckAllowIndexes (); + } + + ImapCommand QueueGetStreamsCommand (FetchStreamCallbackContext ctx, int min, int max, CancellationToken cancellationToken) + { + var command = string.Format ("FETCH {0} (UID BODY.PEEK[])\r\n", GetFetchRange (min, max)); + var ic = new ImapCommand (Engine, cancellationToken, this, command); + ic.RegisterUntaggedHandler ("FETCH", FetchStreamHandler); + ic.UserData = ctx; + + Engine.QueueCommand (ic); + + return ic; + } + + /// + /// Get the streams for the specified messages. + /// + /// + /// Gets the streams for the specified messages. + /// + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// A callback method that gets called for each stream as it is received. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is out of range. + /// -or- + /// is out of range. + /// + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public virtual void GetStreams (int min, int max, ImapFetchStreamCallback callback, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + ValidateArguments (min, max, callback); + + if (min == Count) + return; + + using (var ctx = new FetchStreamCallbackContext (this, callback, progress)) { + var ic = QueueGetStreamsCommand (ctx, min, max, cancellationToken); + + Engine.Run (ic); + + ProcessFetchResponse (ic); + } + } + + /// + /// Asynchronously get the streams for the specified messages. + /// + /// + /// Asynchronously gets the streams for the specified messages. + /// + /// An awaitable task. + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// A callback method that gets called for each stream as it is received. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is out of range. + /// -or- + /// is out of range. + /// + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public virtual async Task GetStreamsAsync (int min, int max, ImapFetchStreamAsyncCallback callback, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + ValidateArguments (min, max, callback); + + if (min == Count) + return; + + using (var ctx = new FetchStreamCallbackContext (this, callback, progress)) { + var ic = QueueGetStreamsCommand (ctx, min, max, cancellationToken); + + await Engine.RunAsync (ic).ConfigureAwait (false); + + ProcessFetchResponse (ic); + } + } + } +} diff --git a/MailKit/Net/Imap/ImapFolderFlags.cs b/MailKit/Net/Imap/ImapFolderFlags.cs new file mode 100644 index 0000000000..718be8ea17 --- /dev/null +++ b/MailKit/Net/Imap/ImapFolderFlags.cs @@ -0,0 +1,829 @@ +// +// ImapFolderFlags.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Linq; +using System.Text; +using System.Threading; +using System.Globalization; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace MailKit.Net.Imap +{ + public partial class ImapFolder + { + static readonly IStoreFlagsRequest AddDeletedFlag = new StoreFlagsRequest (StoreAction.Add, MessageFlags.Deleted) { Silent = true }; + static readonly IStoreFlagsRequest RemoveDeletedFlag = new StoreFlagsRequest (StoreAction.Remove, MessageFlags.Deleted) { Silent = true }; + + static void ProcessUnmodified (ImapCommand ic, ref UniqueIdSet? uids, ulong? modseq) + { + if (modseq.HasValue) { + foreach (var rc in ic.RespCodes.OfType ()) { + if (uids != null && rc.UidSet != null) + uids.AddRange (rc.UidSet); + else + uids = rc.UidSet; + } + } + } + + void ProcessStoreResponse (ImapCommand ic) + { + ProcessResponseCodes (ic, null); + + ic.ThrowIfNotOk ("STORE"); + } + + static IList GetUnmodified (ImapCommand ic, ulong? modseq) + { + if (modseq.HasValue) { + var rc = ic.RespCodes.OfType ().FirstOrDefault (); + + if (rc != null && rc.UidSet != null) { + var unmodified = new int[rc.UidSet.Count]; + for (int i = 0; i < unmodified.Length; i++) + unmodified[i] = (int) (rc.UidSet[i].Id - 1); + + return unmodified; + } + } + + return Array.Empty (); + } + + IEnumerable QueueStoreCommands (IList uids, IStoreFlagsRequest request, CancellationToken cancellationToken) + { + if (uids == null) + throw new ArgumentNullException (nameof (uids)); + + if (request == null) + throw new ArgumentNullException (nameof (request)); + + if (request.UnchangedSince.HasValue && !supportsModSeq) + throw new NotSupportedException ("The ImapFolder does not support mod-sequences."); + + CheckState (true, true); + + if (uids.Count == 0) + return Array.Empty (); + + int numKeywords = request.Keywords != null ? request.Keywords.Count : 0; + string action; + + switch (request.Action) { + case StoreAction.Add: + if ((request.Flags & SettableFlags) == 0 && numKeywords == 0) + return Array.Empty (); + + action = request.Silent ? "+FLAGS.SILENT" : "+FLAGS"; + break; + case StoreAction.Remove: + if ((request.Flags & SettableFlags) == 0 && numKeywords == 0) + return Array.Empty (); + + action = request.Silent ? "-FLAGS.SILENT" : "-FLAGS"; + break; + default: + action = request.Silent ? "FLAGS.SILENT" : "FLAGS"; + break; + } + + var flaglist = ImapUtils.FormatFlagsList (request.Flags & PermanentFlags, request.Keywords != null ? request.Keywords.Count : 0); + var keywordList = request.Keywords != null ? request.Keywords.ToArray () : Array.Empty (); + var @params = string.Empty; + + if (request.UnchangedSince.HasValue) + @params = string.Format (CultureInfo.InvariantCulture, " (UNCHANGEDSINCE {0})", request.UnchangedSince.Value); + + var command = string.Format ("UID STORE %s{0} {1} {2}\r\n", @params, action, flaglist); + + return Engine.QueueCommands (cancellationToken, this, command, uids, keywordList); + } + + /// + /// Store message flags and keywords for a set of messages. + /// + /// + /// Updates the message flags and keywords for a set of messages. + /// + /// The UIDs of the messages that were not updated. + /// The message UIDs. + /// The message flags and keywords to store. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The specified an value + /// but the does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override IList Store (IList uids, IStoreFlagsRequest request, CancellationToken cancellationToken = default) + { + UniqueIdSet? unmodified = null; + + foreach (var ic in QueueStoreCommands (uids, request, cancellationToken)) { + Engine.Run (ic); + + ProcessStoreResponse (ic); + + ProcessUnmodified (ic, ref unmodified, request.UnchangedSince); + } + + if (unmodified == null) + return Array.Empty (); + + return unmodified; + } + + /// + /// Asynchronously store message flags and keywords for a set of messages. + /// + /// + /// Asynchronously updates the message flags and keywords for a set of messages. + /// + /// The UIDs of the messages that were not updated. + /// The message UIDs. + /// The message flags and keywords to store. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The specified an value + /// but the does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override async Task> StoreAsync (IList uids, IStoreFlagsRequest request, CancellationToken cancellationToken = default) + { + UniqueIdSet? unmodified = null; + + foreach (var ic in QueueStoreCommands (uids, request, cancellationToken)) { + await Engine.RunAsync (ic).ConfigureAwait (false); + + ProcessStoreResponse (ic); + + ProcessUnmodified (ic, ref unmodified, request.UnchangedSince); + } + + if (unmodified == null) + return Array.Empty (); + + return unmodified; + } + + bool TryQueueStoreCommand (IList indexes, IStoreFlagsRequest request, CancellationToken cancellationToken, [NotNullWhen (true)] out ImapCommand? ic) + { + if (indexes == null) + throw new ArgumentNullException (nameof (indexes)); + + if (request == null) + throw new ArgumentNullException (nameof (request)); + + if (request.UnchangedSince.HasValue && !supportsModSeq) + throw new NotSupportedException ("The ImapFolder does not support mod-sequences."); + + CheckState (true, true); + + ic = null; + + if (indexes.Count == 0) + return false; + + int numKeywords = request.Keywords != null ? request.Keywords.Count : 0; + string action; + + switch (request.Action) { + case StoreAction.Add: + if ((request.Flags & SettableFlags) == 0 && numKeywords == 0) + return false; + + action = request.Silent ? "+FLAGS.SILENT" : "+FLAGS"; + break; + case StoreAction.Remove: + if ((request.Flags & SettableFlags) == 0 && numKeywords == 0) + return false; + + action = request.Silent ? "-FLAGS.SILENT" : "-FLAGS"; + break; + default: + action = request.Silent ? "FLAGS.SILENT" : "FLAGS"; + break; + } + + var keywordList = request.Keywords != null ? request.Keywords.ToArray () : Array.Empty (); + var command = new StringBuilder ("STORE "); + ImapUtils.FormatIndexSet (Engine, command, indexes); + command.Append (' '); + + if (request.UnchangedSince.HasValue) { + command.Append ("(UNCHANGEDSINCE "); + command.Append (request.UnchangedSince.Value.ToString (CultureInfo.InvariantCulture)); + command.Append (") "); + } + + command.Append (action); + command.Append (' '); + ImapUtils.FormatFlagsList (command, request.Flags & PermanentFlags, request.Keywords != null ? request.Keywords.Count : 0); + command.Append ("\r\n"); + + ic = Engine.QueueCommand (cancellationToken, this, command.ToString (), keywordList); + + return true; + } + + /// + /// Store message flags and keywords for a set of messages. + /// + /// + /// Updates the message flags and keywords for a set of message. + /// + /// The indexes of the messages that were not updated. + /// The message indexes. + /// The message flags and keywords to store. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The specified an value + /// but the does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override IList Store (IList indexes, IStoreFlagsRequest request, CancellationToken cancellationToken = default) + { + if (!TryQueueStoreCommand (indexes, request, cancellationToken, out var ic)) + return Array.Empty (); + + Engine.Run (ic); + + ProcessStoreResponse (ic); + + return GetUnmodified (ic, request.UnchangedSince); + } + + /// + /// Asynchronously store message flags and keywords for a set of messages. + /// + /// + /// Asynchronously updates the message flags and keywords for a set of messages. + /// + /// The indexes of the messages that were not updated. + /// The message indexes. + /// The message flags and keywords to store. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The specified an value + /// but the does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override async Task> StoreAsync (IList indexes, IStoreFlagsRequest request, CancellationToken cancellationToken = default) + { + if (!TryQueueStoreCommand (indexes, request, cancellationToken, out var ic)) + return Array.Empty (); + + await Engine.RunAsync (ic).ConfigureAwait (false); + + ProcessStoreResponse (ic); + + return GetUnmodified (ic, request.UnchangedSince); + } + + void AppendLabelList (StringBuilder command, ISet labels, List args) + { + command.Append ('('); + + if (labels != null) { + int index = 0; + + foreach (var label in labels) { + if (index > 0) + command.Append (' '); + + index++; + + if (label == null) { + command.Append ("NIL"); + continue; + } + + switch (label) { + case "\\AllMail": + case "\\Drafts": + case "\\Important": + case "\\Inbox": + case "\\Spam": + case "\\Sent": + case "\\Starred": + case "\\Trash": + command.Append (label); + break; + default: + command.Append ("%S"); + args.Add (Engine.EncodeMailboxName (label)); + break; + } + } + } + + command.Append (')'); + } + + IEnumerable QueueStoreCommands (IList uids, IStoreLabelsRequest request, CancellationToken cancellationToken) + { + if (uids == null) + throw new ArgumentNullException (nameof (uids)); + + if (request == null) + throw new ArgumentNullException (nameof (request)); + + if ((Engine.Capabilities & ImapCapabilities.GMailExt1) == 0) + throw new NotSupportedException ("The IMAP server does not support the Google Mail extensions."); + + CheckState (true, true); + + if (uids.Count == 0) + return Array.Empty (); + + string action; + + switch (request.Action) { + case StoreAction.Add: + if (request.Labels == null || request.Labels.Count == 0) + return Array.Empty (); + + action = request.Silent ? "+X-GM-LABELS.SILENT" : "+X-GM-LABELS"; + break; + case StoreAction.Remove: + if (request.Labels == null || request.Labels.Count == 0) + return Array.Empty (); + + action = request.Silent ? "-X-GM-LABELS.SILENT" : "-X-GM-LABELS"; + break; + default: + action = request.Silent ? "X-GM-LABELS.SILENT" : "X-GM-LABELS"; + break; + } + + var command = new StringBuilder ("UID STORE %s "); + var args = new List (); + + if (request.UnchangedSince.HasValue) { + command.Append ("(UNCHANGEDSINCE "); + command.Append (request.UnchangedSince.Value.ToString (CultureInfo.InvariantCulture)); + command.Append (") "); + } + + command.Append (action); + command.Append (' '); + AppendLabelList (command, request.Labels, args); + command.Append ("\r\n"); + + return Engine.QueueCommands (cancellationToken, this, command.ToString (), uids, args.ToArray ()); + } + + /// + /// Store GMail-style labels for a set of messages. + /// + /// + /// Updates the GMail-style labels for a set of messages. + /// + /// The UIDs of the messages that were not updated. + /// The message UIDs. + /// The GMail-style labels to store. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The specified an value + /// but the does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override IList Store (IList uids, IStoreLabelsRequest request, CancellationToken cancellationToken = default) + { + UniqueIdSet? unmodified = null; + + foreach (var ic in QueueStoreCommands (uids, request, cancellationToken)) { + Engine.Run (ic); + + ProcessStoreResponse (ic); + + ProcessUnmodified (ic, ref unmodified, request.UnchangedSince); + } + + if (unmodified == null) + return Array.Empty (); + + return unmodified; + } + + /// + /// Asynchronously store GMail-style labels for a set of messages. + /// + /// + /// Asynchronously updates the GMail-style labels for a set of messages. + /// + /// The UIDs of the messages that were not updated. + /// The message UIDs. + /// The GMail-style labels to store. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The specified an value + /// but the does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override async Task> StoreAsync (IList uids, IStoreLabelsRequest request, CancellationToken cancellationToken = default) + { + UniqueIdSet? unmodified = null; + + foreach (var ic in QueueStoreCommands (uids, request, cancellationToken)) { + await Engine.RunAsync (ic).ConfigureAwait (false); + + ProcessStoreResponse (ic); + + ProcessUnmodified (ic, ref unmodified, request.UnchangedSince); + } + + if (unmodified == null) + return Array.Empty (); + + return unmodified; + } + + bool TryQueueStoreCommand (IList indexes, IStoreLabelsRequest request, CancellationToken cancellationToken, [NotNullWhen (true)] out ImapCommand? ic) + { + if (indexes == null) + throw new ArgumentNullException (nameof (indexes)); + + if (request == null) + throw new ArgumentNullException (nameof (request)); + + if ((Engine.Capabilities & ImapCapabilities.GMailExt1) == 0) + throw new NotSupportedException ("The IMAP server does not support the Google Mail extensions."); + + CheckState (true, true); + + ic = null; + + if (indexes.Count == 0) + return false; + + string action; + + switch (request.Action) { + case StoreAction.Add: + if (request.Labels == null || request.Labels.Count == 0) + return false; + + action = request.Silent ? "+X-GM-LABELS.SILENT" : "+X-GM-LABELS"; + break; + case StoreAction.Remove: + if (request.Labels == null || request.Labels.Count == 0) + return false; + + action = request.Silent ? "-X-GM-LABELS.SILENT" : "-X-GM-LABELS"; + break; + default: + action = request.Silent ? "X-GM-LABELS.SILENT" : "X-GM-LABELS"; + break; + } + + var command = new StringBuilder ("STORE "); + var args = new List (); + + ImapUtils.FormatIndexSet (Engine, command, indexes); + command.Append (' '); + + if (request.UnchangedSince.HasValue) { + command.Append ("(UNCHANGEDSINCE "); + command.Append (request.UnchangedSince.Value.ToString (CultureInfo.InvariantCulture)); + command.Append (") "); + } + + command.Append (action); + command.Append (' '); + AppendLabelList (command, request.Labels, args); + command.Append ("\r\n"); + + ic = Engine.QueueCommand (cancellationToken, this, command.ToString (), args.ToArray ()); + + return true; + } + + /// + /// Store GMail-style labels for a set of messages. + /// + /// + /// Updates the GMail-style labels for a set of message. + /// + /// The indexes of the messages that were not updated. + /// The message indexes. + /// The GMail-style labels to store. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The specified an value + /// but the does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override IList Store (IList indexes, IStoreLabelsRequest request, CancellationToken cancellationToken = default) + { + if (!TryQueueStoreCommand (indexes, request, cancellationToken, out var ic)) + return Array.Empty (); + + Engine.Run (ic); + + ProcessStoreResponse (ic); + + return GetUnmodified (ic, request.UnchangedSince); + } + + /// + /// Asynchronously store GMail-style labels for a set of messages. + /// + /// + /// Asynchronously updates the GMail-style labels for a set of messages. + /// + /// The indexes of the messages that were not updated. + /// The message indexes. + /// The GMail-style labels to store. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The specified an value + /// but the does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override async Task> StoreAsync (IList indexes, IStoreLabelsRequest request, CancellationToken cancellationToken = default) + { + if (!TryQueueStoreCommand (indexes, request, cancellationToken, out var ic)) + return Array.Empty (); + + await Engine.RunAsync (ic).ConfigureAwait (false); + + ProcessStoreResponse (ic); + + return GetUnmodified (ic, request.UnchangedSince); + } + } +} diff --git a/MailKit/Net/Imap/ImapFolderSearch.cs b/MailKit/Net/Imap/ImapFolderSearch.cs new file mode 100644 index 0000000000..2f3a3ca308 --- /dev/null +++ b/MailKit/Net/Imap/ImapFolderSearch.cs @@ -0,0 +1,2090 @@ +// +// ImapFolderSearch.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Linq; +using System.Text; +using System.Threading; +using System.Globalization; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +using MailKit.Search; + +namespace MailKit.Net.Imap +{ + public partial class ImapFolder + { + static bool IsAscii (string text) + { + for (int i = 0; i < text.Length; i++) { + if (text[i] > 127) + return false; + } + + return true; + } + + static string FormatDateTime (DateTime date) + { + return date.ToString ("d-MMM-yyyy", CultureInfo.InvariantCulture); + } + + bool IsBadCharset (ImapCommand ic, string? charset) + { + // Note: if `charset` is null, then the charset is actually US-ASCII... + return ic.Response == ImapCommandResponse.No && + ic.RespCodes.Any (rc => rc.Type == ImapResponseCodeType.BadCharset) && + charset != null && !Engine.SupportedCharsets.Contains (charset); + } + + void AddTextArgument (StringBuilder builder, List args, string text, ref string? charset) + { + if (IsAscii (text)) { + builder.Append ("%S"); + args.Add (text); + return; + } + + if (Engine.SupportedCharsets.Contains ("UTF-8")) { + builder.Append ("%S"); + charset = "UTF-8"; + args.Add (text); + return; + } + + // force the text into US-ASCII... + var buffer = new byte[text.Length]; + for (int i = 0; i < text.Length; i++) + buffer[i] = (byte) text[i]; + + builder.Append ("%L"); + args.Add (buffer); + } + + void AddKeywordArgument (StringBuilder builder, List args, string text, ref string? charset) + { + // Note: Technically, the IMAP RFC states that keywords are not allowed to start with '\', + // but some non-rfc-compliant IMAP servers do it anyway, so we need to allow it here. + // See https://github.com/jstedfast/MailKit/issues/1906 for details. + int startIndex = text[0] == '\\' ? 1 : 0; + bool isAtom = true; + + for (int i = startIndex; i < text.Length; i++) { + if (!ImapCommand.IsAtom (text[i])) { + isAtom = false; + break; + } + } + + if (isAtom) { + builder.Append ("%s"); + args.Add (text); + } else { + AddTextArgument (builder, args, text, ref charset); + } + } + + void BuildQuery (StringBuilder builder, SearchQuery query, List args, bool parens, ref string? charset) + { + AnnotationSearchQuery annotation; + NumericSearchQuery numeric; + FilterSearchQuery filter; + HeaderSearchQuery header; + BinarySearchQuery binary; + UnarySearchQuery unary; + DateSearchQuery date; + TextSearchQuery text; + UidSearchQuery uid; + + switch (query.Term) { + case SearchTerm.All: + builder.Append ("ALL"); + break; + case SearchTerm.And: + binary = (BinarySearchQuery) query; + if (parens) + builder.Append ('('); + BuildQuery (builder, binary.Left, args, false, ref charset); + builder.Append (' '); + BuildQuery (builder, binary.Right, args, false, ref charset); + if (parens) + builder.Append (')'); + break; + case SearchTerm.Annotation: + if ((Engine.Capabilities & ImapCapabilities.Annotate) == 0) + throw new NotSupportedException ("The ANNOTATION search term is not supported by the IMAP server."); + + annotation = (AnnotationSearchQuery) query; + builder.Append ("ANNOTATION "); + builder.Append (annotation.Entry); + builder.Append (' '); + builder.Append (annotation.Attribute); + builder.Append (" %S"); + args.Add (annotation.Value); + break; + case SearchTerm.Answered: + builder.Append ("ANSWERED"); + break; + case SearchTerm.BccContains: + text = (TextSearchQuery) query; + builder.Append ("BCC "); + AddTextArgument (builder, args, text.Text, ref charset); + break; + case SearchTerm.BodyContains: + text = (TextSearchQuery) query; + builder.Append ("BODY "); + AddTextArgument (builder, args, text.Text, ref charset); + break; + case SearchTerm.CcContains: + text = (TextSearchQuery) query; + builder.Append ("CC "); + AddTextArgument (builder, args, text.Text, ref charset); + break; + case SearchTerm.Deleted: + builder.Append ("DELETED"); + break; + case SearchTerm.DeliveredAfter: + date = (DateSearchQuery) query; + builder.Append ("SINCE "); + builder.Append (FormatDateTime (date.Date)); + break; + case SearchTerm.DeliveredBefore: + date = (DateSearchQuery) query; + builder.Append ("BEFORE "); + builder.Append (FormatDateTime (date.Date)); + break; + case SearchTerm.DeliveredOn: + date = (DateSearchQuery) query; + builder.Append ("ON "); + builder.Append (FormatDateTime (date.Date)); + break; + case SearchTerm.Draft: + builder.Append ("DRAFT"); + break; + case SearchTerm.Filter: + if ((Engine.Capabilities & ImapCapabilities.Filters) == 0) + throw new NotSupportedException ("The FILTER search term is not supported by the IMAP server."); + + filter = (FilterSearchQuery) query; + builder.Append ("FILTER %S"); + args.Add (filter.Name); + break; + case SearchTerm.Flagged: + builder.Append ("FLAGGED"); + break; + case SearchTerm.FromContains: + text = (TextSearchQuery) query; + builder.Append ("FROM "); + AddTextArgument (builder, args, text.Text, ref charset); + break; + case SearchTerm.Fuzzy: + if ((Engine.Capabilities & ImapCapabilities.FuzzySearch) == 0) + throw new NotSupportedException ("The FUZZY search term is not supported by the IMAP server."); + + builder.Append ("FUZZY "); + unary = (UnarySearchQuery) query; + BuildQuery (builder, unary.Operand, args, true, ref charset); + break; + case SearchTerm.HeaderContains: + header = (HeaderSearchQuery) query; + builder.Append ("HEADER "); + builder.Append (header.Field); + builder.Append (' '); + AddTextArgument (builder, args, header.Value, ref charset); + break; + case SearchTerm.Keyword: + text = (TextSearchQuery) query; + builder.Append ("KEYWORD "); + AddKeywordArgument (builder, args, text.Text, ref charset); + break; + case SearchTerm.LargerThan: + numeric = (NumericSearchQuery) query; + builder.Append ("LARGER "); + builder.Append (numeric.Value.ToString (CultureInfo.InvariantCulture)); + break; + case SearchTerm.MessageContains: + text = (TextSearchQuery) query; + builder.Append ("TEXT "); + AddTextArgument (builder, args, text.Text, ref charset); + break; + case SearchTerm.ModSeq: + numeric = (NumericSearchQuery) query; + builder.Append ("MODSEQ "); + builder.Append (numeric.Value.ToString (CultureInfo.InvariantCulture)); + break; + case SearchTerm.New: + builder.Append ("NEW"); + break; + case SearchTerm.Not: + builder.Append ("NOT "); + unary = (UnarySearchQuery) query; + BuildQuery (builder, unary.Operand, args, true, ref charset); + break; + case SearchTerm.NotAnswered: + builder.Append ("UNANSWERED"); + break; + case SearchTerm.NotDeleted: + builder.Append ("UNDELETED"); + break; + case SearchTerm.NotDraft: + builder.Append ("UNDRAFT"); + break; + case SearchTerm.NotFlagged: + builder.Append ("UNFLAGGED"); + break; + case SearchTerm.NotKeyword: + text = (TextSearchQuery) query; + builder.Append ("UNKEYWORD "); + AddKeywordArgument (builder, args, text.Text, ref charset); + break; + case SearchTerm.NotRecent: + builder.Append ("OLD"); + break; + case SearchTerm.NotSeen: + builder.Append ("UNSEEN"); + break; + case SearchTerm.Older: + if ((Engine.Capabilities & ImapCapabilities.Within) == 0) + throw new NotSupportedException ("The OLDER search term is not supported by the IMAP server."); + + numeric = (NumericSearchQuery) query; + builder.Append ("OLDER "); + builder.Append (numeric.Value.ToString (CultureInfo.InvariantCulture)); + break; + case SearchTerm.Or: + builder.Append ("OR "); + binary = (BinarySearchQuery) query; + BuildQuery (builder, binary.Left, args, true, ref charset); + builder.Append (' '); + BuildQuery (builder, binary.Right, args, true, ref charset); + break; + case SearchTerm.Recent: + builder.Append ("RECENT"); + break; + case SearchTerm.SaveDateSupported: + if ((Engine.Capabilities & ImapCapabilities.SaveDate) == 0) + throw new NotSupportedException ("The SAVEDATESUPPORTED search term is not supported by the IMAP server."); + + builder.Append ("SAVEDATESUPPORTED"); + break; + case SearchTerm.SavedBefore: + if ((Engine.Capabilities & ImapCapabilities.SaveDate) == 0) + throw new NotSupportedException ("The SAVEDBEFORE search term is not supported by the IMAP server."); + + date = (DateSearchQuery) query; + builder.Append ("SAVEDBEFORE "); + builder.Append (FormatDateTime (date.Date)); + break; + case SearchTerm.SavedOn: + if ((Engine.Capabilities & ImapCapabilities.SaveDate) == 0) + throw new NotSupportedException ("The SAVEDON search term is not supported by the IMAP server."); + + date = (DateSearchQuery) query; + builder.Append ("SAVEDON "); + builder.Append (FormatDateTime (date.Date)); + break; + case SearchTerm.SavedSince: + if ((Engine.Capabilities & ImapCapabilities.SaveDate) == 0) + throw new NotSupportedException ("The SAVEDSINCE search term is not supported by the IMAP server."); + + date = (DateSearchQuery) query; + builder.Append ("SAVEDSINCE "); + builder.Append (FormatDateTime (date.Date)); + break; + case SearchTerm.Seen: + builder.Append ("SEEN"); + break; + case SearchTerm.SentBefore: + date = (DateSearchQuery) query; + builder.Append ("SENTBEFORE "); + builder.Append (FormatDateTime (date.Date)); + break; + case SearchTerm.SentOn: + date = (DateSearchQuery) query; + builder.Append ("SENTON "); + builder.Append (FormatDateTime (date.Date)); + break; + case SearchTerm.SentSince: + date = (DateSearchQuery) query; + builder.Append ("SENTSINCE "); + builder.Append (FormatDateTime (date.Date)); + break; + case SearchTerm.SmallerThan: + numeric = (NumericSearchQuery) query; + builder.Append ("SMALLER "); + builder.Append (numeric.Value.ToString (CultureInfo.InvariantCulture)); + break; + case SearchTerm.SubjectContains: + text = (TextSearchQuery) query; + builder.Append ("SUBJECT "); + AddTextArgument (builder, args, text.Text, ref charset); + break; + case SearchTerm.ToContains: + text = (TextSearchQuery) query; + builder.Append ("TO "); + AddTextArgument (builder, args, text.Text, ref charset); + break; + case SearchTerm.Uid: + uid = (UidSearchQuery) query; + builder.Append ("UID "); + builder.Append (UniqueIdSet.ToString (uid.Uids)); + break; + case SearchTerm.Younger: + if ((Engine.Capabilities & ImapCapabilities.Within) == 0) + throw new NotSupportedException ("The YOUNGER search term is not supported by the IMAP server."); + + numeric = (NumericSearchQuery) query; + builder.Append ("YOUNGER "); + builder.Append (numeric.Value.ToString (CultureInfo.InvariantCulture)); + break; + case SearchTerm.GMailMessageId: + if ((Engine.Capabilities & ImapCapabilities.GMailExt1) == 0) + throw new NotSupportedException ("The X-GM-MSGID search term is not supported by the IMAP server."); + + numeric = (NumericSearchQuery) query; + builder.Append ("X-GM-MSGID "); + builder.Append (numeric.Value.ToString (CultureInfo.InvariantCulture)); + break; + case SearchTerm.GMailThreadId: + if ((Engine.Capabilities & ImapCapabilities.GMailExt1) == 0) + throw new NotSupportedException ("The X-GM-THRID search term is not supported by the IMAP server."); + + numeric = (NumericSearchQuery) query; + builder.Append ("X-GM-THRID "); + builder.Append (numeric.Value.ToString (CultureInfo.InvariantCulture)); + break; + case SearchTerm.GMailLabels: + if ((Engine.Capabilities & ImapCapabilities.GMailExt1) == 0) + throw new NotSupportedException ("The X-GM-LABELS search term is not supported by the IMAP server."); + + text = (TextSearchQuery) query; + builder.Append ("X-GM-LABELS "); + AddTextArgument (builder, args, text.Text, ref charset); + break; + case SearchTerm.GMailRaw: + if ((Engine.Capabilities & ImapCapabilities.GMailExt1) == 0) + throw new NotSupportedException ("The X-GM-RAW search term is not supported by the IMAP server."); + + text = (TextSearchQuery) query; + builder.Append ("X-GM-RAW "); + AddTextArgument (builder, args, text.Text, ref charset); + break; + } + } + + string BuildQueryExpression (SearchQuery query, List args, out string? charset) + { + var builder = new StringBuilder (); + + charset = null; + + BuildQuery (builder, query, args, false, ref charset); + + return builder.ToString (); + } + + string BuildSortOrder (IList orderBy) + { + var builder = new StringBuilder (); + + builder.Append ('('); + for (int i = 0; i < orderBy.Count; i++) { + if (builder.Length > 1) + builder.Append (' '); + + if (orderBy[i].Order == SortOrder.Descending) + builder.Append ("REVERSE "); + + switch (orderBy[i].Type) { + case OrderByType.Annotation: + if ((Engine.Capabilities & ImapCapabilities.Annotate) == 0) + throw new NotSupportedException ("The ANNOTATION search term is not supported by the IMAP server."); + + var annotation = (OrderByAnnotation) orderBy[i]; + builder.Append ("ANNOTATION "); + builder.Append (annotation.Entry); + builder.Append (' '); + builder.Append (annotation.Attribute); + break; + case OrderByType.Arrival: builder.Append ("ARRIVAL"); break; + case OrderByType.Cc: builder.Append ("CC"); break; + case OrderByType.Date: builder.Append ("DATE"); break; + case OrderByType.DisplayFrom: + if ((Engine.Capabilities & ImapCapabilities.SortDisplay) == 0) + throw new NotSupportedException ("The IMAP server does not support the SORT=DISPLAY extension."); + + builder.Append ("DISPLAYFROM"); + break; + case OrderByType.DisplayTo: + if ((Engine.Capabilities & ImapCapabilities.SortDisplay) == 0) + throw new NotSupportedException ("The IMAP server does not support the SORT=DISPLAY extension."); + + builder.Append ("DISPLAYTO"); + break; + case OrderByType.From: builder.Append ("FROM"); break; + case OrderByType.Size: builder.Append ("SIZE"); break; + case OrderByType.Subject: builder.Append ("SUBJECT"); break; + case OrderByType.To: builder.Append ("TO"); break; + } + } + builder.Append (')'); + + return builder.ToString (); + } + + static void ParseESearchResults (ImapEngine engine, ImapCommand ic, SearchResults results) + { + var token = engine.ReadToken (ic.CancellationToken); + UniqueId? minValue = null, maxValue = null; + var folder = ic.Folder!; + bool hasCount = false; + int parenDepth = 0; + //bool uid = false; + string atom, tag; + + if (token.Type == ImapTokenType.OpenParen) { + // optional search correlator + do { + token = engine.ReadToken (ic.CancellationToken); + + if (token.Type == ImapTokenType.CloseParen) + break; + + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); + + atom = (string) token.Value; + + if (atom == "TAG") { + token = engine.ReadToken (ic.CancellationToken); + + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapTokenType.QString, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); + + tag = (string) token.Value; + + if (tag != ic.Tag) + throw new ImapProtocolException ("Unexpected TAG value in untagged ESEARCH response: " + tag); + } + } while (true); + + token = engine.ReadToken (ic.CancellationToken); + } + + if (token.Type == ImapTokenType.Atom && ((string) token.Value) == "UID") { + token = engine.ReadToken (ic.CancellationToken); + //uid = true; + } + + do { + if (token.Type == ImapTokenType.CloseParen) { + if (parenDepth == 0) + throw ImapEngine.UnexpectedToken (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); + + token = engine.ReadToken (ic.CancellationToken); + parenDepth--; + } + + if (token.Type == ImapTokenType.Eoln) { + // unget the eoln token + engine.UngetToken (token); + break; + } + + if (token.Type == ImapTokenType.OpenParen) { + token = engine.ReadToken (ic.CancellationToken); + parenDepth++; + } + + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); + + atom = (string) token.Value; + + token = engine.ReadToken (ic.CancellationToken); + + if (atom.Equals ("RELEVANCY", StringComparison.OrdinalIgnoreCase)) { + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); + + results.Relevancy = new List (); + + do { + token = engine.ReadToken (ic.CancellationToken); + + if (token.Type == ImapTokenType.CloseParen) + break; + + var score = ImapEngine.ParseNumber (token, true, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); + + if (score > 100) + throw ImapEngine.UnexpectedToken (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); + + results.Relevancy.Add ((byte) score); + } while (true); + } else if (atom.Equals ("MODSEQ", StringComparison.OrdinalIgnoreCase)) { + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); + + results.ModSeq = ImapEngine.ParseNumber64 (token, false, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + } else if (atom.Equals ("COUNT", StringComparison.OrdinalIgnoreCase)) { + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); + + var count = ImapEngine.ParseNumber (token, false, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + + results.Count = (int) count; + hasCount = true; + } else if (atom.Equals ("MIN", StringComparison.OrdinalIgnoreCase)) { + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); + + var min = ImapEngine.ParseNumber (token, true, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + + results.Min = new UniqueId (folder.UidValidity, min); + } else if (atom.Equals ("MAX", StringComparison.OrdinalIgnoreCase)) { + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); + + var max = ImapEngine.ParseNumber (token, true, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + + results.Max = new UniqueId (folder.UidValidity, max); + } else if (atom.Equals ("ALL", StringComparison.OrdinalIgnoreCase)) { + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); + + var uids = ImapEngine.ParseUidSet (token, folder.UidValidity, out minValue, out maxValue, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + if (!hasCount) + results.Count = uids.Count; + + results.UniqueIds = uids; + } else { + throw ImapEngine.UnexpectedToken (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); + } + + token = engine.ReadToken (ic.CancellationToken); + } while (true); + + if (!results.Min.HasValue) + results.Min = minValue; + + if (!results.Max.HasValue) + results.Max = maxValue; + } + + static async Task ParseESearchResultsAsync (ImapEngine engine, ImapCommand ic, SearchResults results) + { + var token = await engine.ReadTokenAsync (ic.CancellationToken).ConfigureAwait (false); + UniqueId? minValue = null, maxValue = null; + var folder = ic.Folder!; + bool hasCount = false; + int parenDepth = 0; + //bool uid = false; + string atom, tag; + + if (token.Type == ImapTokenType.OpenParen) { + // optional search correlator + do { + token = await engine.ReadTokenAsync (ic.CancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.CloseParen) + break; + + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); + + atom = (string) token.Value; + + if (atom == "TAG") { + token = await engine.ReadTokenAsync (ic.CancellationToken).ConfigureAwait (false); + + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapTokenType.QString, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); + + tag = (string) token.Value; + + if (tag != ic.Tag) + throw new ImapProtocolException ("Unexpected TAG value in untagged ESEARCH response: " + tag); + } + } while (true); + + token = await engine.ReadTokenAsync (ic.CancellationToken).ConfigureAwait (false); + } + + if (token.Type == ImapTokenType.Atom && ((string) token.Value) == "UID") { + token = await engine.ReadTokenAsync (ic.CancellationToken).ConfigureAwait (false); + //uid = true; + } + + do { + if (token.Type == ImapTokenType.CloseParen) { + if (parenDepth == 0) + throw ImapEngine.UnexpectedToken (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); + + token = await engine.ReadTokenAsync (ic.CancellationToken).ConfigureAwait (false); + parenDepth--; + } + + if (token.Type == ImapTokenType.Eoln) { + // unget the eoln token + engine.UngetToken (token); + break; + } + + if (token.Type == ImapTokenType.OpenParen) { + token = await engine.ReadTokenAsync (ic.CancellationToken).ConfigureAwait (false); + parenDepth++; + } + + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); + + atom = (string) token.Value; + + token = await engine.ReadTokenAsync (ic.CancellationToken).ConfigureAwait (false); + + if (atom.Equals ("RELEVANCY", StringComparison.OrdinalIgnoreCase)) { + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); + + results.Relevancy = new List (); + + do { + token = await engine.ReadTokenAsync (ic.CancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.CloseParen) + break; + + var score = ImapEngine.ParseNumber (token, true, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); + + if (score > 100) + throw ImapEngine.UnexpectedToken (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); + + results.Relevancy.Add ((byte) score); + } while (true); + } else if (atom.Equals ("MODSEQ", StringComparison.OrdinalIgnoreCase)) { + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); + + results.ModSeq = ImapEngine.ParseNumber64 (token, false, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + } else if (atom.Equals ("COUNT", StringComparison.OrdinalIgnoreCase)) { + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); + + var count = ImapEngine.ParseNumber (token, false, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + + results.Count = (int) count; + hasCount = true; + } else if (atom.Equals ("MIN", StringComparison.OrdinalIgnoreCase)) { + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); + + var min = ImapEngine.ParseNumber (token, true, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + + results.Min = new UniqueId (folder.UidValidity, min); + } else if (atom.Equals ("MAX", StringComparison.OrdinalIgnoreCase)) { + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); + + var max = ImapEngine.ParseNumber (token, true, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + + results.Max = new UniqueId (folder.UidValidity, max); + } else if (atom.Equals ("ALL", StringComparison.OrdinalIgnoreCase)) { + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); + + var uids = ImapEngine.ParseUidSet (token, folder.UidValidity, out minValue, out maxValue, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + if (!hasCount) + results.Count = uids.Count; + + results.UniqueIds = uids; + } else { + throw ImapEngine.UnexpectedToken (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); + } + + token = await engine.ReadTokenAsync (ic.CancellationToken).ConfigureAwait (false); + } while (true); + + if (!results.Min.HasValue) + results.Min = minValue; + + if (!results.Max.HasValue) + results.Max = maxValue; + } + + static Task UntaggedESearchHandler (ImapEngine engine, ImapCommand ic, int index, bool doAsync) + { + var results = (SearchResults) ic.UserData!; + + if (doAsync) + return ParseESearchResultsAsync (engine, ic, results); + + ParseESearchResults (engine, ic, results); + + return Task.CompletedTask; + } + + static void ParseSearchResults (ImapEngine engine, ImapCommand ic, SearchResults results) + { + var folder = ic.Folder!; + var uids = results.UniqueIds; + uint min = uint.MaxValue; + uint uid, max = 0; + ImapToken token; + + do { + token = engine.PeekToken (ic.CancellationToken); + + // keep reading UIDs until we get to the end of the line or until we get a "(MODSEQ ####)" + if (token.Type == ImapTokenType.Eoln || token.Type == ImapTokenType.OpenParen) + break; + + token = engine.ReadToken (ic.CancellationToken); + + uid = ImapEngine.ParseNumber (token, true, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "SEARCH", token); + uids.Add (new UniqueId (folder.UidValidity, uid)); + min = Math.Min (min, uid); + max = Math.Max (max, uid); + } while (true); + + if (token.Type == ImapTokenType.OpenParen) { + engine.ReadToken (ic.CancellationToken); + + do { + token = engine.ReadToken (ic.CancellationToken); + + if (token.Type == ImapTokenType.CloseParen) + break; + + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "SEARCH", token); + + var atom = (string) token.Value; + + if (atom.Equals ("MODSEQ", StringComparison.OrdinalIgnoreCase)) { + token = engine.ReadToken (ic.CancellationToken); + + results.ModSeq = ImapEngine.ParseNumber64 (token, false, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + } + + token = engine.PeekToken (ic.CancellationToken); + } while (token.Type != ImapTokenType.Eoln); + } + + results.UniqueIds = uids; + results.Count = uids.Count; + if (uids.Count > 0) { + results.Min = new UniqueId (folder.UidValidity, min); + results.Max = new UniqueId (folder.UidValidity, max); + } + } + + static async Task ParseSearchResultsAsync (ImapEngine engine, ImapCommand ic, SearchResults results) + { + var folder = ic.Folder!; + var uids = results.UniqueIds; + uint min = uint.MaxValue; + uint uid, max = 0; + ImapToken token; + + do { + token = await engine.PeekTokenAsync (ic.CancellationToken).ConfigureAwait (false); + + // keep reading UIDs until we get to the end of the line or until we get a "(MODSEQ ####)" + if (token.Type == ImapTokenType.Eoln || token.Type == ImapTokenType.OpenParen) + break; + + token = await engine.ReadTokenAsync (ic.CancellationToken).ConfigureAwait (false); + + uid = ImapEngine.ParseNumber (token, true, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "SEARCH", token); + uids.Add (new UniqueId (folder.UidValidity, uid)); + min = Math.Min (min, uid); + max = Math.Max (max, uid); + } while (true); + + if (token.Type == ImapTokenType.OpenParen) { + await engine.ReadTokenAsync (ic.CancellationToken).ConfigureAwait (false); + + do { + token = await engine.ReadTokenAsync (ic.CancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.CloseParen) + break; + + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "SEARCH", token); + + var atom = (string) token.Value; + + if (atom.Equals ("MODSEQ", StringComparison.OrdinalIgnoreCase)) { + token = await engine.ReadTokenAsync (ic.CancellationToken).ConfigureAwait (false); + + results.ModSeq = ImapEngine.ParseNumber64 (token, false, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + } + + token = await engine.PeekTokenAsync (ic.CancellationToken).ConfigureAwait (false); + } while (token.Type != ImapTokenType.Eoln); + } + + results.UniqueIds = uids; + results.Count = uids.Count; + if (uids.Count > 0) { + results.Min = new UniqueId (folder.UidValidity, min); + results.Max = new UniqueId (folder.UidValidity, max); + } + } + + static Task UntaggedSearchHandler (ImapEngine engine, ImapCommand ic, int index, bool doAsync) + { + var results = (SearchResults) ic.UserData!; + + if (doAsync) + return ParseSearchResultsAsync (engine, ic, results); + + ParseSearchResults (engine, ic, results); + + return Task.CompletedTask; + } + + ImapCommand QueueSearchCommand (string query, CancellationToken cancellationToken) + { + if (query == null) + throw new ArgumentNullException (nameof (query)); + + query = query.Trim (); + + if (query.Length == 0) + throw new ArgumentException ("Cannot search using an empty query.", nameof (query)); + + CheckState (true, false); + + var command = "UID SEARCH " + query + "\r\n"; + var ic = new ImapCommand (Engine, cancellationToken, this, command); + if ((Engine.Capabilities & ImapCapabilities.ESearch) != 0) + ic.RegisterUntaggedHandler ("ESEARCH", UntaggedESearchHandler); + + // Note: always register the untagged SEARCH handler because some servers will brokenly + // respond with "* SEARCH ..." instead of "* ESEARCH ..." even when using the extended + // search syntax. + ic.RegisterUntaggedHandler ("SEARCH", UntaggedSearchHandler); + ic.UserData = new SearchResults (UidValidity, SortOrder.Ascending); + + Engine.QueueCommand (ic); + + return ic; + } + + SearchResults ProcessSearchResponse (ImapCommand ic) + { + ProcessResponseCodes (ic, null); + + ic.ThrowIfNotOk ("SEARCH"); + + return (SearchResults) ic.UserData!; + } + + /// + /// Search the folder for messages matching the specified query. + /// + /// + /// Sends a UID SEARCH command with the specified query passed directly to the IMAP server + /// with no interpretation by MailKit. This means that the query may contain any arguments that a + /// UID SEARCH command is allowed to have according to the IMAP specifications and any + /// extensions that are supported, including RETURN parameters. + /// + /// An array of matching UIDs. + /// The search query. + /// The cancellation token. + /// + /// is . + /// + /// + /// is an empty string. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public virtual SearchResults Search (string query, CancellationToken cancellationToken = default) + { + var ic = QueueSearchCommand (query, cancellationToken); + + Engine.Run (ic); + + return ProcessSearchResponse (ic); + } + + /// + /// Asynchronously search the folder for messages matching the specified query. + /// + /// + /// Sends a UID SEARCH command with the specified query passed directly to the IMAP server + /// with no interpretation by MailKit. This means that the query may contain any arguments that a + /// UID SEARCH command is allowed to have according to the IMAP specifications and any + /// extensions that are supported, including RETURN parameters. + /// + /// An array of matching UIDs. + /// The search query. + /// The cancellation token. + /// + /// is . + /// + /// + /// is an empty string. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public virtual async Task SearchAsync (string query, CancellationToken cancellationToken = default) + { + var ic = QueueSearchCommand (query, cancellationToken); + + await Engine.RunAsync (ic).ConfigureAwait (false); + + return ProcessSearchResponse (ic); + } + + ImapCommand QueueSearchCommand (SearchOptions options, SearchQuery query, CancellationToken cancellationToken, out string? charset) + { + if (query == null) + throw new ArgumentNullException (nameof (query)); + + CheckState (true, false); + + if (options != SearchOptions.None && (Engine.Capabilities & ImapCapabilities.ESearch) == 0) + throw new NotSupportedException ("The IMAP server does not support the ESEARCH extension."); + + var args = new List (); + var optimized = query.Optimize (new ImapSearchQueryOptimizer ()); + var expr = BuildQueryExpression (optimized, args, out charset); + var command = "UID SEARCH "; + + if ((Engine.Capabilities & ImapCapabilities.ESearch) != 0) { + command += "RETURN ("; + + if (options != SearchOptions.All && options != SearchOptions.None) { + if ((options & SearchOptions.All) != 0) + command += "ALL "; + if ((options & SearchOptions.Relevancy) != 0) + command += "RELEVANCY "; + if ((options & SearchOptions.Count) != 0) + command += "COUNT "; + if ((options & SearchOptions.Min) != 0) + command += "MIN "; + if ((options & SearchOptions.Max) != 0) + command += "MAX "; + command = command.TrimEnd (); + } else { + command += "ALL"; + } + + command += ") "; + } + + if (charset != null && args.Count > 0 && !Engine.UTF8Enabled) + command += "CHARSET " + charset + " "; + + command += expr + "\r\n"; + + var ic = new ImapCommand (Engine, cancellationToken, this, command, args.ToArray ()) { + UserData = new SearchResults (UidValidity, SortOrder.Ascending) + }; + + if ((Engine.Capabilities & ImapCapabilities.ESearch) != 0) + ic.RegisterUntaggedHandler ("ESEARCH", UntaggedESearchHandler); + + // Note: always register the untagged SEARCH handler because some servers will brokenly + // respond with "* SEARCH ..." instead of "* ESEARCH ..." even when using the extended + // search syntax. + ic.RegisterUntaggedHandler ("SEARCH", UntaggedSearchHandler); + + Engine.QueueCommand (ic); + + return ic; + } + + bool TryProcessSearchResponse (ImapCommand ic, string? charset, bool retry, [NotNullWhen (true)] out SearchResults? results) + { + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) { + if (retry && IsBadCharset (ic, charset)) { + results = null; + return false; + } + + throw ImapCommandException.Create ("SEARCH", ic); + } + + results = (SearchResults) ic.UserData!; + + return true; + } + + SearchResults Search (SearchOptions options, SearchQuery query, bool retry, CancellationToken cancellationToken) + { + var ic = QueueSearchCommand (options, query, cancellationToken, out string? charset); + + Engine.Run (ic); + + if (TryProcessSearchResponse (ic, charset, retry, out var results)) + return results; + + return Search (options, query, false, cancellationToken); + } + + /// + /// Search the folder for messages matching the specified query. + /// + /// + /// Searches the folder for messages matching the specified query, + /// returning only the specified search results. + /// + /// The search results. + /// The search options. + /// The search query. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more search terms in the are not supported by the IMAP server. + /// -or- + /// The IMAP server does not support the ESEARCH extension. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override SearchResults Search (SearchOptions options, SearchQuery query, CancellationToken cancellationToken = default) + { + return Search (options, query, true, cancellationToken); + } + + async Task SearchAsync (SearchOptions options, SearchQuery query, bool retry, CancellationToken cancellationToken) + { + var ic = QueueSearchCommand (options, query, cancellationToken, out string? charset); + + await Engine.RunAsync (ic).ConfigureAwait (false); + + if (TryProcessSearchResponse (ic, charset, retry, out var results)) + return results; + + return await SearchAsync (options, query, false, cancellationToken).ConfigureAwait (false); + } + + /// + /// Asynchronously search the folder for messages matching the specified query. + /// + /// + /// Searches the folder for messages matching the specified query, + /// returning only the specified search results. + /// + /// The search results. + /// The search options. + /// The search query. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more search terms in the are not supported by the IMAP server. + /// -or- + /// The IMAP server does not support the ESEARCH extension. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task SearchAsync (SearchOptions options, SearchQuery query, CancellationToken cancellationToken = default) + { + return SearchAsync (options, query, true, cancellationToken); + } + + ImapCommand QueueSortCommand (string query, CancellationToken cancellationToken) + { + if (query == null) + throw new ArgumentNullException (nameof (query)); + + query = query.Trim (); + + if (query.Length == 0) + throw new ArgumentException ("Cannot sort using an empty query.", nameof (query)); + + if ((Engine.Capabilities & ImapCapabilities.Sort) == 0) + throw new NotSupportedException ("The IMAP server does not support the SORT extension."); + + CheckState (true, false); + + var command = "UID SORT " + query + "\r\n"; + var ic = new ImapCommand (Engine, cancellationToken, this, command); + if ((Engine.Capabilities & ImapCapabilities.ESort) != 0) + ic.RegisterUntaggedHandler ("ESEARCH", UntaggedESearchHandler); + ic.RegisterUntaggedHandler ("SORT", UntaggedSearchHandler); + ic.UserData = new SearchResults (UidValidity); + + Engine.QueueCommand (ic); + + return ic; + } + + SearchResults ProcessSortResponse (ImapCommand ic) + { + ProcessResponseCodes (ic, null); + + ic.ThrowIfNotOk ("SORT"); + + return (SearchResults) ic.UserData!; + } + + /// + /// Sort messages matching the specified query. + /// + /// + /// Sends a UID SORT command with the specified query passed directly to the IMAP server + /// with no interpretation by MailKit. This means that the query may contain any arguments that a + /// UID SORT command is allowed to have according to the IMAP specifications and any + /// extensions that are supported, including RETURN parameters. + /// + /// An array of matching UIDs. + /// The search query. + /// The cancellation token. + /// + /// is . + /// + /// + /// is an empty string. + /// + /// + /// The IMAP server does not support the SORT extension. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public virtual SearchResults Sort (string query, CancellationToken cancellationToken = default) + { + var ic = QueueSortCommand (query, cancellationToken); + + Engine.Run (ic); + + return ProcessSortResponse (ic); + } + + /// + /// Asynchronously sort messages matching the specified query. + /// + /// + /// Sends a UID SORT command with the specified query passed directly to the IMAP server + /// with no interpretation by MailKit. This means that the query may contain any arguments that a + /// UID SORT command is allowed to have according to the IMAP specifications and any + /// extensions that are supported, including RETURN parameters. + /// + /// An array of matching UIDs. + /// The search query. + /// The cancellation token. + /// + /// is . + /// + /// + /// is an empty string. + /// + /// + /// The IMAP server does not support the SORT extension. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public virtual async Task SortAsync (string query, CancellationToken cancellationToken = default) + { + var ic = QueueSortCommand (query, cancellationToken); + + await Engine.RunAsync (ic).ConfigureAwait (false); + + return ProcessSortResponse (ic); + } + + ImapCommand QueueSortCommand (SearchQuery query, IList orderBy, CancellationToken cancellationToken, out string? charset) + { + if (query == null) + throw new ArgumentNullException (nameof (query)); + + if (orderBy == null) + throw new ArgumentNullException (nameof (orderBy)); + + if (orderBy.Count == 0) + throw new ArgumentException ("No sort order provided.", nameof (orderBy)); + + CheckState (true, false); + + if ((Engine.Capabilities & ImapCapabilities.Sort) == 0) + throw new NotSupportedException ("The IMAP server does not support the SORT extension."); + + var args = new List (); + var optimized = query.Optimize (new ImapSearchQueryOptimizer ()); + var expr = BuildQueryExpression (optimized, args, out charset); + var order = BuildSortOrder (orderBy); + var command = "UID SORT "; + + if ((Engine.Capabilities & ImapCapabilities.ESort) != 0) + command += "RETURN (ALL) "; + + command += order + " " + (charset ?? "US-ASCII") + " " + expr + "\r\n"; + + var ic = new ImapCommand (Engine, cancellationToken, this, command, args.ToArray ()) { + UserData = new SearchResults (UidValidity) + }; + + if ((Engine.Capabilities & ImapCapabilities.ESort) != 0) + ic.RegisterUntaggedHandler ("ESEARCH", UntaggedESearchHandler); + else + ic.RegisterUntaggedHandler ("SORT", UntaggedSearchHandler); + + Engine.QueueCommand (ic); + + return ic; + } + + bool TryProcessSortResponse (ImapCommand ic, string? charset, bool retry, [NotNullWhen (true)] out IList? results) + { + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) { + if (retry && IsBadCharset (ic, charset)) { + results = null; + return false; + } + + throw ImapCommandException.Create ("SORT", ic); + } + + results = ((SearchResults) ic.UserData!).UniqueIds; + + return true; + } + + IList Sort (SearchQuery query, IList orderBy, bool retry, CancellationToken cancellationToken) + { + var ic = QueueSortCommand (query, orderBy, cancellationToken, out string? charset); + + Engine.Run (ic); + + if (TryProcessSortResponse (ic, charset, retry, out IList? results)) + return results; + + return Sort (query, orderBy, false, cancellationToken); + } + + /// + /// Sort messages matching the specified query. + /// + /// + /// The returned array of unique identifiers will be sorted in the preferred order and + /// can be used with . + /// + /// An array of matching UIDs in the specified sort order. + /// The search query. + /// The sort order. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// is empty. + /// + /// + /// One or more search terms in the are not supported by the IMAP server. + /// -or- + /// The server does not support the SORT extension. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override IList Sort (SearchQuery query, IList orderBy, CancellationToken cancellationToken = default) + { + return Sort (query, orderBy, true, cancellationToken); + } + + async Task> SortAsync (SearchQuery query, IList orderBy, bool retry, CancellationToken cancellationToken) + { + var ic = QueueSortCommand (query, orderBy, cancellationToken, out string? charset); + + await Engine.RunAsync (ic).ConfigureAwait (false); + + if (TryProcessSortResponse (ic, charset, retry, out IList? results)) + return results; + + return await SortAsync (query, orderBy, false, cancellationToken).ConfigureAwait (false); + } + + /// + /// Asynchronously sort messages matching the specified query. + /// + /// + /// The returned array of unique identifiers will be sorted in the preferred order and + /// can be used with . + /// + /// An array of matching UIDs in the specified sort order. + /// The search query. + /// The sort order. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// is empty. + /// + /// + /// One or more search terms in the are not supported by the IMAP server. + /// -or- + /// The server does not support the SORT extension. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task> SortAsync (SearchQuery query, IList orderBy, CancellationToken cancellationToken = default) + { + return SortAsync (query, orderBy, true, cancellationToken); + } + + ImapCommand QueueSortCommand (SearchOptions options, SearchQuery query, IList orderBy, CancellationToken cancellationToken, out string? charset) + { + if (query == null) + throw new ArgumentNullException (nameof (query)); + + if (orderBy == null) + throw new ArgumentNullException (nameof (orderBy)); + + if (orderBy.Count == 0) + throw new ArgumentException ("No sort order provided.", nameof (orderBy)); + + CheckState (true, false); + + if (options != SearchOptions.None && (Engine.Capabilities & ImapCapabilities.ESort) == 0) + throw new NotSupportedException ("The IMAP server does not support the ESORT extension."); + + var args = new List (); + var optimized = query.Optimize (new ImapSearchQueryOptimizer ()); + var expr = BuildQueryExpression (optimized, args, out charset); + var order = BuildSortOrder (orderBy); + var command = "UID SORT "; + + if ((Engine.Capabilities & ImapCapabilities.ESort) != 0) { + command += "RETURN ("; + + if (options != SearchOptions.All && options != SearchOptions.None) { + if ((options & SearchOptions.All) != 0) + command += "ALL "; + if ((options & SearchOptions.Relevancy) != 0) + command += "RELEVANCY "; + if ((options & SearchOptions.Count) != 0) + command += "COUNT "; + if ((options & SearchOptions.Min) != 0) + command += "MIN "; + if ((options & SearchOptions.Max) != 0) + command += "MAX "; + command = command.TrimEnd (); + } else { + command += "ALL"; + } + + command += ") "; + } + + command += order + " " + (charset ?? "US-ASCII") + " " + expr + "\r\n"; + + var ic = new ImapCommand (Engine, cancellationToken, this, command, args.ToArray ()) { + UserData = new SearchResults (UidValidity) + }; + + if ((Engine.Capabilities & ImapCapabilities.ESort) != 0) + ic.RegisterUntaggedHandler ("ESEARCH", UntaggedESearchHandler); + else + ic.RegisterUntaggedHandler ("SORT", UntaggedSearchHandler); + + Engine.QueueCommand (ic); + + return ic; + } + + bool TryProcessSortResponse (ImapCommand ic, string? charset, bool retry, [NotNullWhen (true)] out SearchResults? results) + { + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) { + if (retry && IsBadCharset (ic, charset)) { + results = null; + return false; + } + + throw ImapCommandException.Create ("SORT", ic); + } + + results = (SearchResults) ic.UserData!; + + return true; + } + + SearchResults Sort (SearchOptions options, SearchQuery query, IList orderBy, bool retry, CancellationToken cancellationToken) + { + var ic = QueueSortCommand (options, query, orderBy, cancellationToken, out string? charset); + + Engine.Run (ic); + + if (TryProcessSortResponse (ic, charset, retry, out SearchResults? results)) + return results; + + return Sort (options, query, orderBy, false, cancellationToken); + } + + /// + /// Sort messages matching the specified query. + /// + /// + /// Searches the folder for messages matching the specified query, returning the search results in the specified sort order. + /// + /// The search results. + /// The search options. + /// The search query. + /// The sort order. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// is empty. + /// + /// + /// One or more search terms in the are not supported by the IMAP server. + /// -or- + /// The IMAP server does not support the ESORT extension. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override SearchResults Sort (SearchOptions options, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default) + { + return Sort (options, query, orderBy, true, cancellationToken); + } + + async Task SortAsync (SearchOptions options, SearchQuery query, IList orderBy, bool retry, CancellationToken cancellationToken) + { + var ic = QueueSortCommand (options, query, orderBy, cancellationToken, out string? charset); + + await Engine.RunAsync (ic).ConfigureAwait (false); + + if (TryProcessSortResponse (ic, charset, retry, out SearchResults? results)) + return results; + + return await SortAsync (options, query, orderBy, false, cancellationToken).ConfigureAwait (false); + } + + /// + /// Asynchronously sort messages matching the specified query. + /// + /// + /// Searches the folder for messages matching the specified query, returning the search results in the specified sort order. + /// + /// The search results. + /// The search options. + /// The search query. + /// The sort order. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// is empty. + /// + /// + /// One or more search terms in the are not supported by the IMAP server. + /// -or- + /// The IMAP server does not support the ESORT extension. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task SortAsync (SearchOptions options, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default) + { + return SortAsync (options, query, orderBy, true, cancellationToken); + } + + ImapCommand QueueThreadCommand (ThreadingAlgorithm algorithm, SearchQuery query, CancellationToken cancellationToken, out string? charset) + { + if ((Engine.Capabilities & ImapCapabilities.Thread) == 0) + throw new NotSupportedException ("The IMAP server does not support the THREAD extension."); + + if (!Engine.ThreadingAlgorithms.Contains (algorithm)) + throw new ArgumentOutOfRangeException (nameof (algorithm), "The specified threading algorithm is not supported."); + + if (query == null) + throw new ArgumentNullException (nameof (query)); + + CheckState (true, false); + + var method = algorithm.ToString ().ToUpperInvariant (); + var args = new List (); + var optimized = query.Optimize (new ImapSearchQueryOptimizer ()); + var expr = BuildQueryExpression (optimized, args, out charset); + var command = "UID THREAD " + method + " " + (charset ?? "US-ASCII") + " "; + + command += expr + "\r\n"; + + var ic = new ImapCommand (Engine, cancellationToken, this, command, args.ToArray ()); + ic.RegisterUntaggedHandler ("THREAD", ImapUtils.UntaggedThreadHandler); + + Engine.QueueCommand (ic); + + return ic; + } + + bool TryProcessThreadResponse (ImapCommand ic, string? charset, bool retry, [NotNullWhen (true)] out IList? threads) + { + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) { + if (retry && IsBadCharset (ic, charset)) { + threads = null; + return false; + } + + throw ImapCommandException.Create ("THREAD", ic); + } + + threads = (IList) ic.UserData! ?? Array.Empty (); + + return true; + } + + IList Thread (ThreadingAlgorithm algorithm, SearchQuery query, bool retry, CancellationToken cancellationToken) + { + var ic = QueueThreadCommand (algorithm, query, cancellationToken, out string? charset); + + Engine.Run (ic); + + if (TryProcessThreadResponse (ic, charset, retry, out IList? threads)) + return threads; + + return Thread (algorithm, query, false, cancellationToken); + } + + /// + /// Thread the messages in the folder that match the search query using the specified threading algorithm. + /// + /// + /// The can be used with methods such as + /// . + /// + /// An array of message threads. + /// The threading algorithm to use. + /// The search query. + /// The cancellation token. + /// + /// is not supported. + /// + /// + /// is . + /// + /// + /// One or more search terms in the are not supported by the IMAP server. + /// -or- + /// The server does not support the THREAD extension. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override IList Thread (ThreadingAlgorithm algorithm, SearchQuery query, CancellationToken cancellationToken = default) + { + return Thread (algorithm, query, true, cancellationToken); + } + + async Task> ThreadAsync (ThreadingAlgorithm algorithm, SearchQuery query, bool retry, CancellationToken cancellationToken) + { + var ic = QueueThreadCommand (algorithm, query, cancellationToken, out string? charset); + + await Engine.RunAsync (ic).ConfigureAwait (false); + + if (TryProcessThreadResponse (ic, charset, retry, out IList? threads)) + return threads; + + return await ThreadAsync (algorithm, query, false, cancellationToken).ConfigureAwait (false); + } + + /// + /// Asynchronously thread the messages in the folder that match the search query using the specified threading algorithm. + /// + /// + /// The can be used with methods such as + /// . + /// + /// An array of message threads. + /// The threading algorithm to use. + /// The search query. + /// The cancellation token. + /// + /// is not supported. + /// + /// + /// is . + /// + /// + /// One or more search terms in the are not supported by the IMAP server. + /// -or- + /// The server does not support the THREAD extension. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task> ThreadAsync (ThreadingAlgorithm algorithm, SearchQuery query, CancellationToken cancellationToken = default) + { + return ThreadAsync (algorithm, query, true, cancellationToken); + } + + ImapCommand? QueueThreadCommand (IList uids, ThreadingAlgorithm algorithm, SearchQuery query, CancellationToken cancellationToken, out string? charset) + { + if (uids == null) + throw new ArgumentNullException (nameof (uids)); + + if ((Engine.Capabilities & ImapCapabilities.Thread) == 0) + throw new NotSupportedException ("The IMAP server does not support the THREAD extension."); + + if (!Engine.ThreadingAlgorithms.Contains (algorithm)) + throw new ArgumentOutOfRangeException (nameof (algorithm), "The specified threading algorithm is not supported."); + + if (query == null) + throw new ArgumentNullException (nameof (query)); + + CheckState (true, false); + + if (uids.Count == 0) { + charset = null; + return null; + } + + var method = algorithm.ToString ().ToUpperInvariant (); + var set = UniqueIdSet.ToString (uids); + var args = new List (); + var optimized = query.Optimize (new ImapSearchQueryOptimizer ()); + var expr = BuildQueryExpression (optimized, args, out charset); + var command = "UID THREAD " + method + " " + (charset ?? "US-ASCII") + " "; + + command += "UID " + set + " " + expr + "\r\n"; + + var ic = new ImapCommand (Engine, cancellationToken, this, command, args.ToArray ()); + ic.RegisterUntaggedHandler ("THREAD", ImapUtils.UntaggedThreadHandler); + + Engine.QueueCommand (ic); + + return ic; + } + + IList Thread (IList uids, ThreadingAlgorithm algorithm, SearchQuery query, bool retry, CancellationToken cancellationToken) + { + var ic = QueueThreadCommand (uids, algorithm, query, cancellationToken, out string? charset); + + if (ic == null) + return Array.Empty (); + + Engine.Run (ic); + + if (TryProcessThreadResponse (ic, charset!, retry, out IList? threads)) + return threads; + + return Thread (uids, algorithm, query, false, cancellationToken); + } + + /// + /// Thread the messages in the folder that match the search query using the specified threading algorithm. + /// + /// + /// The can be used with methods such as + /// . + /// + /// An array of message threads. + /// The subset of UIDs + /// The threading algorithm to use. + /// The search query. + /// The cancellation token. + /// + /// is not supported. + /// + /// + /// is . + /// -or- + /// is . + /// + /// + /// is empty. + /// -or- + /// One or more of the is invalid. + /// + /// + /// One or more search terms in the are not supported by the IMAP server. + /// -or- + /// The server does not support the THREAD extension. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override IList Thread (IList uids, ThreadingAlgorithm algorithm, SearchQuery query, CancellationToken cancellationToken = default) + { + return Thread (uids, algorithm, query, true, cancellationToken); + } + + async Task> ThreadAsync (IList uids, ThreadingAlgorithm algorithm, SearchQuery query, bool retry, CancellationToken cancellationToken) + { + var ic = QueueThreadCommand (uids, algorithm, query, cancellationToken, out string? charset); + + if (ic == null) + return Array.Empty (); + + await Engine.RunAsync (ic).ConfigureAwait (false); + + if (TryProcessThreadResponse (ic, charset!, retry, out IList? threads)) + return threads; + + return await ThreadAsync (uids, algorithm, query, false, cancellationToken).ConfigureAwait (false); + } + + /// + /// Asynchronously thread the messages in the folder that match the search query using the specified threading algorithm. + /// + /// + /// The can be used with methods such as + /// . + /// + /// An array of message threads. + /// The subset of UIDs + /// The threading algorithm to use. + /// The search query. + /// The cancellation token. + /// + /// is not supported. + /// + /// + /// is . + /// -or- + /// is . + /// + /// + /// is empty. + /// -or- + /// One or more of the is invalid. + /// + /// + /// One or more search terms in the are not supported by the IMAP server. + /// -or- + /// The server does not support the THREAD extension. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task> ThreadAsync (IList uids, ThreadingAlgorithm algorithm, SearchQuery query, CancellationToken cancellationToken = default) + { + return ThreadAsync (uids, algorithm, query, true, cancellationToken); + } + } +} diff --git a/MailKit/Net/Imap/ImapIdleContext.cs b/MailKit/Net/Imap/ImapIdleContext.cs new file mode 100644 index 0000000000..902a3c2f25 --- /dev/null +++ b/MailKit/Net/Imap/ImapIdleContext.cs @@ -0,0 +1,170 @@ +// +// ImapIdleContext.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace MailKit.Net.Imap { + /// + /// An IMAP IDLE context. + /// + /// + /// An IMAP IDLE command does not work like normal commands. Unlike most commands, + /// the IDLE command does not end until the client sends a separate "DONE" command. + /// In order to facilitate this, the way this works is that the consumer of MailKit's + /// IMAP APIs provides a 'doneToken' which signals to the command-processing loop to + /// send the "DONE" command. Since, like every other IMAP command, it is also necessary to + /// provide a means of cancelling the IDLE command, it becomes necessary to link the + /// 'doneToken' and the 'cancellationToken' together. + /// + sealed class ImapIdleContext : IDisposable + { + static readonly byte[] DoneCommand = Encoding.ASCII.GetBytes ("DONE\r\n"); + CancellationTokenRegistration registration; + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The IMAP engine. + /// The done token. + /// The cancellation token. + public ImapIdleContext (ImapEngine engine, CancellationToken doneToken, CancellationToken cancellationToken) + { + CancellationToken = cancellationToken; + DoneToken = doneToken; + Engine = engine; + } + + /// + /// Get the engine. + /// + /// + /// Gets the engine. + /// + /// The engine. + public ImapEngine Engine { + get; private set; + } + + /// + /// Get the cancellation token. + /// + /// + /// Get the cancellation token. + /// + /// The cancellation token. + public CancellationToken CancellationToken { + get; private set; + } + + /// + /// Get the done token. + /// + /// + /// Gets the done token. + /// + /// The done token. + public CancellationToken DoneToken { + get; private set; + } + +#if false + /// + /// Get whether or not cancellation has been requested. + /// + /// + /// Gets whether or not cancellation has been requested. + /// + /// if cancellation has been requested; otherwise, . + public bool IsCancellationRequested { + get { return CancellationToken.IsCancellationRequested; } + } + + /// + /// Get whether or not the IDLE command should be ended. + /// + /// + /// Gets whether or not the IDLE command should be ended. + /// + /// if the IDLE command should end; otherwise, . + public bool IsDoneRequested { + get { return DoneToken.IsCancellationRequested; } + } +#endif + + void IdleComplete () + { + if (Engine.IsIdle) { + try { + Engine.Stream.Write (DoneCommand, 0, DoneCommand.Length, CancellationToken); + Engine.Stream.Flush (CancellationToken); + } catch { + return; + } + + Engine.State = ImapEngineState.Selected; + } + } + + /// + /// Callback method to be used as the ImapCommand's ContinuationHandler. + /// + /// + /// Callback method to be used as the ImapCommand's ContinuationHandler. + /// + /// The ImapEngine. + /// The ImapCommand. + /// The text. + /// if the command is being run asynchronously; otherwise, . + /// + public Task ContinuationHandler (ImapEngine engine, ImapCommand ic, string text, bool doAsync) + { + Engine.State = ImapEngineState.Idle; + + registration = DoneToken.Register (IdleComplete); + + return Task.CompletedTask; + } + + /// + /// Releases all resource used by the object. + /// + /// Call when you are finished using the . The + /// method leaves the in an unusable state. After + /// calling , you must release all references to the + /// so the garbage collector can reclaim the memory that the + /// was occupying. + public void Dispose () + { + registration.Dispose (); + } + } +} diff --git a/MailKit/Net/Imap/ImapImplementation.cs b/MailKit/Net/Imap/ImapImplementation.cs index e45f24a776..557f652e72 100644 --- a/MailKit/Net/Imap/ImapImplementation.cs +++ b/MailKit/Net/Imap/ImapImplementation.cs @@ -1,9 +1,9 @@ -// +// // ImapImplementation.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -50,14 +50,12 @@ public class ImapImplementation /// public ImapImplementation () { - Properties = new Dictionary (); + Properties = new Dictionary (); } - string GetProperty (string property) + string? GetProperty (string property) { - string value; - - Properties.TryGetValue (property, out value); + Properties.TryGetValue (property, out var value); return value; } @@ -72,7 +70,7 @@ string GetProperty (string property) /// /// /// The properties. - public Dictionary Properties { + public Dictionary Properties { get; private set; } @@ -86,7 +84,7 @@ public Dictionary Properties { /// /// /// The program name. - public string Name { + public string? Name { get { return GetProperty ("name"); } set { Properties["name"] = value; } } @@ -101,7 +99,7 @@ public string Name { /// /// /// The program version. - public string Version { + public string? Version { get { return GetProperty ("version"); } set { Properties["version"] = value; } } @@ -113,7 +111,7 @@ public string Version { /// Gets or sets the name of the operating system. /// /// The name of the operation system. - public string OS { + public string? OS { get { return GetProperty ("os"); } set { Properties["os"] = value; } } @@ -125,7 +123,7 @@ public string OS { /// Gets or sets the version of the operating system. /// /// The version of the operation system. - public string OSVersion { + public string? OSVersion { get { return GetProperty ("os-version"); } set { Properties["os-version"] = value; } } @@ -137,7 +135,7 @@ public string OSVersion { /// Gets or sets the name of the vendor. /// /// The name of the vendor. - public string Vendor { + public string? Vendor { get { return GetProperty ("vendor"); } set { Properties["vendor"] = value; } } @@ -149,7 +147,7 @@ public string Vendor { /// Gets or sets the support URL. /// /// The support URL. - public string SupportUrl { + public string? SupportUrl { get { return GetProperty ("support-url"); } set { Properties["support-url"] = value; } } @@ -161,7 +159,7 @@ public string SupportUrl { /// Gets or sets the postal address of the vendor. /// /// The postal address. - public string Address { + public string? Address { get { return GetProperty ("address"); } set { Properties["address"] = value; } } @@ -173,7 +171,7 @@ public string Address { /// Gets or sets the release date of the program. /// /// The release date. - public string ReleaseDate { + public string? ReleaseDate { get { return GetProperty ("date"); } set { Properties["date"] = value; } } @@ -185,7 +183,7 @@ public string ReleaseDate { /// Gets or sets the command used to start the program. /// /// The command used to start the program. - public string Command { + public string? Command { get { return GetProperty ("command"); } set { Properties["command"] = value; } } @@ -197,7 +195,7 @@ public string Command { /// Gets or sets the command-line arguments used to start the program. /// /// The command-line arguments used to start the program. - public string Arguments { + public string? Arguments { get { return GetProperty ("arguments"); } set { Properties["arguments"] = value; } } @@ -209,7 +207,7 @@ public string Arguments { /// Get or set the environment variables available to the program. /// /// The environment variables. - public string Environment { + public string? Environment { get { return GetProperty ("environment"); } set { Properties["environment"] = value; } } diff --git a/MailKit/Net/Imap/ImapLiteral.cs b/MailKit/Net/Imap/ImapLiteral.cs new file mode 100644 index 0000000000..fc4356a955 --- /dev/null +++ b/MailKit/Net/Imap/ImapLiteral.cs @@ -0,0 +1,202 @@ +// +// ImapLiteral.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Threading; +using System.Threading.Tasks; + +using MimeKit; +using MimeKit.IO; + +namespace MailKit.Net.Imap { + enum ImapLiteralType + { + String, + //Stream, + MimeMessage + } + + /// + /// An IMAP literal object. + /// + /// + /// The literal can be a string, byte[], Stream, or a MimeMessage. + /// + class ImapLiteral + { + public readonly ImapLiteralType Type; + public readonly object Literal; + readonly FormatOptions format; + readonly Action update; + + static void DefaultUpdate (int value) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The formatting options. + /// The message. + /// The progress update action. + public ImapLiteral (FormatOptions options, MimeMessage message, Action action) + { + format = options.Clone (); + format.NewLineFormat = NewLineFormat.Dos; + + update = action; + + Type = ImapLiteralType.MimeMessage; + Literal = message; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The formatting options. + /// The literal. + public ImapLiteral (FormatOptions options, byte[] literal) + { + format = options.Clone (); + format.NewLineFormat = NewLineFormat.Dos; + + update = DefaultUpdate; + + Type = ImapLiteralType.String; + Literal = literal; + } + + /// + /// Get the length of the literal, in bytes. + /// + /// + /// Gets the length of the literal, in bytes. + /// + /// The length. + public long Length { + get { + if (Type == ImapLiteralType.String) + return ((byte[]) Literal).Length; + + using (var measure = new MeasuringStream ()) { + //if (Type == ImapLiteralType.Stream) { + // var stream = (Stream) Literal; + // stream.CopyTo (measure, 4096); + // stream.Position = 0; + + // return measure.Length; + //} + + ((MimeMessage) Literal).WriteTo (format, measure); + + return measure.Length; + } + } + } + + /// + /// Write the literal to the specified stream. + /// + /// + /// Writes the literal to the specified stream. + /// + /// The stream. + /// The cancellation token. + public void WriteTo (ImapStream stream, CancellationToken cancellationToken) + { + if (Type == ImapLiteralType.String) { + var bytes = (byte[]) Literal; + + stream.Write (bytes, 0, bytes.Length, cancellationToken); + stream.Flush (cancellationToken); + return; + } + + //if (Type == ImapLiteralType.Stream) { + // var literal = (Stream) Literal; + // var buf = new byte[4096]; + // int nread; + + // while ((nread = literal.Read (buf, 0, buf.Length)) > 0) + // stream.Write (buf, 0, nread, cancellationToken); + + // stream.Flush (cancellationToken); + // return; + //} + + var message = (MimeMessage) Literal; + + using (var s = new ProgressStream (stream, update)) { + message.WriteTo (format, s, cancellationToken); + s.Flush (cancellationToken); + } + } + + /// + /// Asynchronously write the literal to the specified stream. + /// + /// + /// Asynchronously writes the literal to the specified stream. + /// + /// The stream. + /// The cancellation token. + public async Task WriteToAsync (ImapStream stream, CancellationToken cancellationToken) + { + if (Type == ImapLiteralType.String) { + var bytes = (byte[]) Literal; + + await stream.WriteAsync (bytes, 0, bytes.Length, cancellationToken).ConfigureAwait (false); + await stream.FlushAsync (cancellationToken).ConfigureAwait (false); + return; + } + + //if (Type == ImapLiteralType.Stream) { + // var literal = (Stream) Literal; + // var buf = new byte[4096]; + // int nread; + + // while ((nread = await literal.ReadAsync (buf, 0, buf.Length, cancellationToken).ConfigureAwait (false)) > 0) + // await stream.WriteAsync (buf, 0, nread, cancellationToken).ConfigureAwait (false); + + // await stream.FlushAsync (cancellationToken).ConfigureAwait (false); + // return; + //} + + var message = (MimeMessage) Literal; + + using (var s = new ProgressStream (stream, update)) { + await message.WriteToAsync (format, s, cancellationToken).ConfigureAwait (false); + await s.FlushAsync (cancellationToken).ConfigureAwait (false); + } + } + } +} diff --git a/MailKit/Net/Imap/ImapProtocolException.cs b/MailKit/Net/Imap/ImapProtocolException.cs index b79977e555..cb37c2ae49 100644 --- a/MailKit/Net/Imap/ImapProtocolException.cs +++ b/MailKit/Net/Imap/ImapProtocolException.cs @@ -1,9 +1,9 @@ -// +// // ImapException.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -54,9 +54,10 @@ public class ImapProtocolException : ProtocolException /// The serialization info. /// The streaming context. /// - /// is null. + /// is . /// [SecuritySafeCritical] + [Obsolete ("This API supports obsolete formatter-based serialization. It should not be called or extended by application code.")] protected ImapProtocolException (SerializationInfo info, StreamingContext context) : base (info, context) { } @@ -101,9 +102,37 @@ public ImapProtocolException () /// /// Gets or sets whether or not this exception was thrown due to an unexpected token. /// - /// true if an unexpected token was encountered; otherwise, false. + /// if an unexpected token was encountered; otherwise, . internal bool UnexpectedToken { get; set; } + + /// + /// Create a new based on the state. + /// + /// + /// Create a new based on the state. + /// + /// A new protocol exception. + /// The command state. + internal static ImapProtocolException Create (ImapCommand ic) + { + string? message = null; + + if (string.IsNullOrEmpty (ic.ResponseText)) { + for (int i = ic.RespCodes.Count - 1; i >= 0; i--) { + if (ic.RespCodes[i].IsError && !string.IsNullOrEmpty (ic.RespCodes[i].Message)) { + message = ic.RespCodes[i].Message; + break; + } + } + + message ??= string.Empty; + } else { + message = ic.ResponseText!; + } + + return ic.Exception != null ? new ImapProtocolException (message, ic.Exception) : new ImapProtocolException (message); + } } } diff --git a/MailKit/Net/Imap/ImapResponseCode.cs b/MailKit/Net/Imap/ImapResponseCode.cs index 1d79a1026f..1c3b7b29bb 100644 --- a/MailKit/Net/Imap/ImapResponseCode.cs +++ b/MailKit/Net/Imap/ImapResponseCode.cs @@ -1,9 +1,9 @@ -// +// // ImapResponseCode.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -24,6 +24,9 @@ // THE SOFTWARE. // +using System; +using System.Collections.Generic; + namespace MailKit.Net.Imap { enum ImapResponseCodeType : byte { Alert, @@ -39,9 +42,6 @@ enum ImapResponseCodeType : byte { UidValidity, Unseen, - // RESP-CODES introduced in rfc2086: - MyRights, - // RESP-CODES introduced in rfc2221: Referral, @@ -121,17 +121,24 @@ enum ImapResponseCodeType : byte { // RESP-CODES introduced in rfc6154: UseAttr, + // RESP-CODES introduced in rfc8474: + MailboxId, + + // GMail-specific RESP-CODES + WebAlert, + Unknown = 255 } class ImapResponseCode { public readonly ImapResponseCodeType Type; - public readonly bool IsError; + public bool IsTagged, IsError; public string Message; internal ImapResponseCode (ImapResponseCodeType type, bool isError) { + Message = string.Empty; IsError = isError; Type = type; } @@ -166,14 +173,14 @@ public static ImapResponseCode Create (ImapResponseCodeType type) case ImapResponseCodeType.Closed: return new ImapResponseCode (type, false); case ImapResponseCodeType.NotSaved: return new ImapResponseCode (type, true); case ImapResponseCodeType.BadComparator: return new ImapResponseCode (type, true); - case ImapResponseCodeType.Annotate: return new ImapResponseCode (type, false); - case ImapResponseCodeType.Annotations: return new ImapResponseCode (type, false); + case ImapResponseCodeType.Annotate: return new AnnotateResponseCode (type); + case ImapResponseCodeType.Annotations: return new AnnotationsResponseCode (type); case ImapResponseCodeType.MaxConvertMessages: return new MaxConvertResponseCode (type); case ImapResponseCodeType.MaxConvertParts: return new MaxConvertResponseCode (type); case ImapResponseCodeType.TempFail: return new ImapResponseCode (type, true); case ImapResponseCodeType.NoUpdate: return new NoUpdateResponseCode (type); case ImapResponseCodeType.Metadata: return new MetadataResponseCode (type); - case ImapResponseCodeType.NotificationOverflow: return new ImapResponseCode (type, true); + case ImapResponseCodeType.NotificationOverflow: return new ImapResponseCode (type, false); case ImapResponseCodeType.BadEvent: return new ImapResponseCode (type, true); case ImapResponseCodeType.UndefinedFilter: return new UndefinedFilterResponseCode (type); case ImapResponseCodeType.Unavailable: return new ImapResponseCode (type, true); @@ -194,6 +201,8 @@ public static ImapResponseCode Create (ImapResponseCodeType type) case ImapResponseCodeType.AlreadyExists: return new ImapResponseCode (type, true); case ImapResponseCodeType.NonExistent: return new ImapResponseCode (type, true); case ImapResponseCodeType.UseAttr: return new ImapResponseCode (type, true); + case ImapResponseCodeType.MailboxId: return new MailboxIdResponseCode (type); + case ImapResponseCodeType.WebAlert: return new WebAlertResponseCode (type); default: return new ImapResponseCode (type, true); } } @@ -201,8 +210,8 @@ public static ImapResponseCode Create (ImapResponseCodeType type) class NewNameResponseCode : ImapResponseCode { - public string OldName; - public string NewName; + public string? OldName; + public string? NewName; internal NewNameResponseCode (ImapResponseCodeType type) : base (type, false) { @@ -211,10 +220,12 @@ internal NewNameResponseCode (ImapResponseCodeType type) : base (type, false) class PermanentFlagsResponseCode : ImapResponseCode { + public readonly HashSet Keywords; public MessageFlags Flags; internal PermanentFlagsResponseCode (ImapResponseCodeType type) : base (type, false) { + Keywords = new HashSet (StringComparer.Ordinal); } } @@ -247,7 +258,7 @@ internal UnseenResponseCode (ImapResponseCodeType type) : base (type, false) class AppendUidResponseCode : UidValidityResponseCode { - public UniqueIdSet UidSet; + public UniqueIdSet? UidSet; internal AppendUidResponseCode (ImapResponseCodeType type) : base (type) { @@ -256,7 +267,7 @@ internal AppendUidResponseCode (ImapResponseCodeType type) : base (type) class CopyUidResponseCode : UidValidityResponseCode { - public UniqueIdSet SrcUidSet, DestUidSet; + public UniqueIdSet? SrcUidSet, DestUidSet; internal CopyUidResponseCode (ImapResponseCodeType type) : base (type) { @@ -265,7 +276,7 @@ internal CopyUidResponseCode (ImapResponseCodeType type) : base (type) class BadUrlResponseCode : ImapResponseCode { - public string BadUrl; + public string? BadUrl; internal BadUrlResponseCode (ImapResponseCodeType type) : base (type, true) { @@ -283,7 +294,7 @@ internal HighestModSeqResponseCode (ImapResponseCodeType type) : base (type, fal class ModifiedResponseCode : ImapResponseCode { - public UniqueIdSet UidSet; + public UniqueIdSet? UidSet; internal ModifiedResponseCode (ImapResponseCodeType type) : base (type, false) { @@ -292,7 +303,7 @@ internal ModifiedResponseCode (ImapResponseCodeType type) : base (type, false) class MaxConvertResponseCode : ImapResponseCode { - public int MaxConvert; + public uint MaxConvert; internal MaxConvertResponseCode (ImapResponseCodeType type) : base (type, true) { @@ -301,13 +312,39 @@ internal MaxConvertResponseCode (ImapResponseCodeType type) : base (type, true) class NoUpdateResponseCode : ImapResponseCode { - public string Tag; + public string? Tag; internal NoUpdateResponseCode (ImapResponseCodeType type) : base (type, true) { } } + enum AnnotateResponseCodeSubType + { + TooBig, + TooMany + } + + class AnnotateResponseCode : ImapResponseCode + { + public AnnotateResponseCodeSubType SubType; + + internal AnnotateResponseCode (ImapResponseCodeType type) : base (type, true) + { + } + } + + class AnnotationsResponseCode : ImapResponseCode + { + public AnnotationAccess Access; + public AnnotationScope Scopes; + public uint MaxSize; + + internal AnnotationsResponseCode (ImapResponseCodeType type) : base (type, false) + { + } + } + enum MetadataResponseCodeSubType { LongEntries, @@ -321,7 +358,6 @@ class MetadataResponseCode : ImapResponseCode public MetadataResponseCodeSubType SubType; public uint Value; - // FIXME: the LONGENTRIES code is not an error internal MetadataResponseCode (ImapResponseCodeType type) : base (type, true) { } @@ -329,10 +365,28 @@ internal MetadataResponseCode (ImapResponseCodeType type) : base (type, true) class UndefinedFilterResponseCode : ImapResponseCode { - public string Name; + public string? Name; internal UndefinedFilterResponseCode (ImapResponseCodeType type) : base (type, true) { } } + + class MailboxIdResponseCode : ImapResponseCode + { + public string? MailboxId; + + internal MailboxIdResponseCode (ImapResponseCodeType type) : base (type, false) + { + } + } + + class WebAlertResponseCode : ImapResponseCode + { + public Uri? WebUri; + + internal WebAlertResponseCode (ImapResponseCodeType type) : base (type, false) + { + } + } } diff --git a/MailKit/Net/Imap/ImapSearchQueryOptimizer.cs b/MailKit/Net/Imap/ImapSearchQueryOptimizer.cs index a64d4c68a9..9b3e79de09 100644 --- a/MailKit/Net/Imap/ImapSearchQueryOptimizer.cs +++ b/MailKit/Net/Imap/ImapSearchQueryOptimizer.cs @@ -1,9 +1,9 @@ -// +// // ImapSearchQueryOptimizer.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -37,10 +37,10 @@ public SearchQuery Reduce (SearchQuery expr) var and = (BinarySearchQuery) expr; if (and.Left.Term == SearchTerm.All) - return and.Right; + return and.Right.Optimize (this); if (and.Right.Term == SearchTerm.All) - return and.Left; + return and.Left.Optimize (this); } else if (expr.Term == SearchTerm.Or) { var or = (BinarySearchQuery) expr; @@ -53,6 +53,7 @@ public SearchQuery Reduce (SearchQuery expr) var unary = (UnarySearchQuery) expr; switch (unary.Operand.Term) { + case SearchTerm.Not: return ((UnarySearchQuery) unary.Operand).Operand.Optimize (this); case SearchTerm.NotAnswered: return SearchQuery.Answered; case SearchTerm.Answered: return SearchQuery.NotAnswered; case SearchTerm.NotDeleted: return SearchQuery.Deleted; diff --git a/MailKit/Net/Imap/ImapStream.cs b/MailKit/Net/Imap/ImapStream.cs index 3a09cfd5ae..b37e66ee4b 100644 --- a/MailKit/Net/Imap/ImapStream.cs +++ b/MailKit/Net/Imap/ImapStream.cs @@ -1,9 +1,9 @@ -// +// // ImapStream.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -26,21 +26,16 @@ using System; using System.IO; -using System.Text; using System.Threading; -using Buffer = System.Buffer; - -#if NETFX_CORE -using Windows.Storage.Streams; -using Windows.Networking.Sockets; -using Socket = Windows.Networking.Sockets.StreamSocket; -#else -using System.Net.Security; using System.Net.Sockets; -#endif +using System.Globalization; +using System.Threading.Tasks; +using System.Collections.Generic; using MimeKit.IO; +using Buffer = System.Buffer; + namespace MailKit.Net.Imap { /// /// An enumeration of the possible IMAP streaming modes. @@ -64,10 +59,8 @@ enum ImapStreamMode { class ImapStream : Stream, ICancellableStream { - // Note: GMail's IMAP implementation is broken and does not quote strings with ']' like it should. - public const string GMailLabelSpecials = "(){%*\\\"\n"; - public const string AtomSpecials = "](){%*\\\"\n"; - public const string DefaultSpecials = "[" + AtomSpecials; + public const string AtomSpecials = "(){%*\\\""; + public const string DefaultSpecials = "[]" + AtomSpecials; const int ReadAheadSize = 128; const int BlockSize = 4096; const int PadSize = 4; @@ -81,9 +74,9 @@ class ImapStream : Stream, ICancellableStream readonly byte[] output = new byte[BlockSize]; int outputIndex; + readonly Stack tokens; readonly IProtocolLogger logger; int literalDataLeft; - ImapToken nextToken; bool disposed; /// @@ -93,36 +86,33 @@ class ImapStream : Stream, ICancellableStream /// Creates a new . /// /// The underlying network stream. - /// The underlying network socket. /// The protocol logger. - public ImapStream (Stream source, Socket socket, IProtocolLogger protocolLogger) + public ImapStream (Stream source, IProtocolLogger protocolLogger) { + tokens = new Stack (); logger = protocolLogger; IsConnected = true; Stream = source; - Socket = socket; } /// - /// Get or sets the underlying network stream. + /// Get the underlying network stream. /// /// - /// Gets or sets the underlying network stream. + /// Gets the underlying network stream. /// /// The underlying network stream. public Stream Stream { - get; internal set; + get; private set; } - /// - /// Get the underlying network socket. - /// - /// - /// Gets the underlying network socket. - /// - /// The underlying network socket. - public Socket Socket { - get; private set; + internal void SetStream (Stream stream) + { + Stream = stream; + + // reset internal buffering + inputIndex = ReadAheadSize; + inputEnd = ReadAheadSize; } /// @@ -145,6 +135,7 @@ public ImapStreamMode Mode { /// The length of the literal. public int LiteralLength { get { return literalDataLeft; } + internal set { literalDataLeft = value; } } /// @@ -153,9 +144,9 @@ public int LiteralLength { /// /// Gets whether or not the stream is connected. /// - /// true if the stream is connected; otherwise, false. + /// if the stream is connected; otherwise, . public bool IsConnected { - get; internal set; + get; private set; } /// @@ -164,7 +155,7 @@ public bool IsConnected { /// /// Gets whether the stream supports reading. /// - /// true if the stream supports reading; otherwise, false. + /// if the stream supports reading; otherwise, . public override bool CanRead { get { return Stream.CanRead; } } @@ -175,7 +166,7 @@ public override bool CanRead { /// /// Gets whether the stream supports writing. /// - /// true if the stream supports writing; otherwise, false. + /// if the stream supports writing; otherwise, . public override bool CanWrite { get { return Stream.CanWrite; } } @@ -186,7 +177,7 @@ public override bool CanWrite { /// /// Gets whether the stream supports seeking. /// - /// true if the stream supports seeking; otherwise, false. + /// if the stream supports seeking; otherwise, . public override bool CanSeek { get { return false; } } @@ -197,7 +188,7 @@ public override bool CanSeek { /// /// Gets whether the stream supports I/O timeouts. /// - /// true if the stream supports I/O timeouts; otherwise, false. + /// if the stream supports I/O timeouts; otherwise, . public override bool CanTimeout { get { return Stream.CanTimeout; } } @@ -247,7 +238,7 @@ public override int WriteTimeout { /// public override long Position { get { return Stream.Position; } - set { Stream.Position = value; } + set { throw new NotSupportedException (); } } /// @@ -268,35 +259,14 @@ public override long Length { get { return Stream.Length; } } - void Poll (SelectMode mode, CancellationToken cancellationToken) - { -#if NETFX_CORE - cancellationToken.ThrowIfCancellationRequested (); -#else - if (!cancellationToken.CanBeCanceled) - return; - - if (Socket != null) { - do { - cancellationToken.ThrowIfCancellationRequested (); - // wait 1/4 second and then re-check for cancellation - } while (!Socket.Poll (250000, mode)); - } else { - cancellationToken.ThrowIfCancellationRequested (); - } -#endif - } - - unsafe int ReadAhead (int atleast, CancellationToken cancellationToken) + bool AlignReadAheadBuffer (int atleast, out int left, out int start, out int end) { - int left = inputEnd - inputIndex; + left = inputEnd - inputIndex; + start = inputStart; + end = inputEnd; if (left >= atleast) - return left; - - int start = inputStart; - int end = inputEnd; - int nread; + return false; if (left > 0) { int index = inputIndex; @@ -326,31 +296,57 @@ unsafe int ReadAhead (int atleast, CancellationToken cancellationToken) end = input.Length - PadSize; + return true; + } + + int ReadAhead (int atleast, CancellationToken cancellationToken) + { + if (!AlignReadAheadBuffer (atleast, out int left, out int start, out int end)) + return left; + try { -#if !NETFX_CORE - bool buffered = !(Stream is NetworkStream); -#else - bool buffered = true; -#endif + var network = Stream as NetworkStream; + int nread; - if (buffered) { - cancellationToken.ThrowIfCancellationRequested (); + cancellationToken.ThrowIfCancellationRequested (); - nread = Stream.Read (input, start, end - start); - } else { - Poll (SelectMode.SelectRead, cancellationToken); + network?.Poll (SelectMode.SelectRead, cancellationToken); - nread = Stream.Read (input, start, end - start); + if ((nread = Stream.Read (input, start, end - start)) > 0) { + logger.LogServer (input, start, nread); + inputEnd += nread; + } else { + throw new ImapProtocolException ("The IMAP server has unexpectedly disconnected."); } - if (nread > 0) { + if (network == null) + cancellationToken.ThrowIfCancellationRequested (); + } catch { + IsConnected = false; + throw; + } + + return inputEnd - inputIndex; + } + + async ValueTask ReadAheadAsync (int atleast, CancellationToken cancellationToken) + { + if (!AlignReadAheadBuffer (atleast, out int left, out int start, out int end)) + return left; + + try { + int nread; + + cancellationToken.ThrowIfCancellationRequested (); + + if ((nread = await Stream.ReadAsync (input, start, end - start, cancellationToken).ConfigureAwait (false)) > 0) { logger.LogServer (input, start, nread); inputEnd += nread; } else { throw new ImapProtocolException ("The IMAP server has unexpectedly disconnected."); } - if (buffered) + if (Stream is not NetworkStream) cancellationToken.ThrowIfCancellationRequested (); } catch { IsConnected = false; @@ -389,12 +385,12 @@ void CheckDisposed () /// The number of bytes to read. /// The cancellation token. /// - /// is null. + /// is . /// /// /// is less than zero or greater than the length of . /// -or- - /// The is not large enough to contain bytes strting + /// The is not large enough to contain bytes starting /// at the specified . /// /// @@ -449,12 +445,12 @@ public int Read (byte[] buffer, int offset, int count, CancellationToken cancell /// The buffer offset. /// The number of bytes to read. /// - /// is null. + /// is . /// /// /// is less than zero or greater than the length of . /// -or- - /// The is not large enough to contain bytes strting + /// The is not large enough to contain bytes starting /// at the specified . /// /// @@ -471,232 +467,350 @@ public override int Read (byte[] buffer, int offset, int count) return Read (buffer, offset, count, CancellationToken.None); } + /// + /// Reads a sequence of bytes from the stream and advances the position + /// within the stream by the number of bytes read. + /// + /// + /// Reads a sequence of bytes from the stream and advances the position + /// within the stream by the number of bytes read. + /// + /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many + /// bytes are not currently available, or zero (0) if the end of the stream has been reached. + /// The buffer. + /// The buffer offset. + /// The number of bytes to read. + /// The cancellation token. + /// + /// is . + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes starting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public override async Task ReadAsync (byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + CheckDisposed (); + + ValidateArguments (buffer, offset, count); + + if (Mode != ImapStreamMode.Literal) + return 0; + + count = Math.Min (count, literalDataLeft); + + int length = inputEnd - inputIndex; + int n; + + if (length < count && length <= ReadAheadSize) + await ReadAheadAsync (BlockSize, cancellationToken).ConfigureAwait (false); + + length = inputEnd - inputIndex; + n = Math.Min (count, length); + + Buffer.BlockCopy (input, inputIndex, buffer, offset, n); + literalDataLeft -= n; + inputIndex += n; + + if (literalDataLeft == 0) + Mode = ImapStreamMode.Token; + + return n; + } + static bool IsAtom (byte c, string specials) { - return !IsCtrl (c) && !IsWhiteSpace (c) && specials.IndexOf ((char) c) == -1; + return !IsCtrl (c) && c != (byte) ' ' && specials.IndexOf ((char) c) == -1; } static bool IsCtrl (byte c) { - return c <= 0x1f || c >= 0x7f; + return c <= 0x1f || c == 0x7f; } static bool IsWhiteSpace (byte c) { - return c == (byte) ' ' || c == (byte) '\t' || c == (byte) '\r'; + return c == (byte) ' ' || c == (byte) '\r'; } - unsafe ImapToken ReadQuotedStringToken (byte* inbuf, CancellationToken cancellationToken) + bool TryReadQuotedString (ByteArrayBuilder builder, ref bool escaped) + { + do { + while (inputIndex < inputEnd) { + if (input[inputIndex] == (byte) '"' && !escaped) + break; + + if (input[inputIndex] == (byte) '\\' && !escaped) { + escaped = true; + } else { + builder.Append (input[inputIndex]); + escaped = false; + } + + inputIndex++; + } + + if (inputIndex + 1 < inputEnd) { + // skip over closing '"' + inputIndex++; + + // Note: Some IMAP servers do not properly escape double-quotes inside + // of a qstring token and so, as an attempt at working around this + // problem, check that the closing '"' character is not immediately + // followed by any character that we would expect immediately following + // a qstring token. + // + // See https://github.com/jstedfast/MailKit/issues/485 for details. + if ("]) \r\n".IndexOf ((char) input[inputIndex]) != -1) + return true; + + builder.Append ((byte) '"'); + continue; + } + + return false; + } while (true); + } + + ImapToken ReadQuotedStringToken (CancellationToken cancellationToken) { - byte* inptr = inbuf + inputIndex; - byte* inend = inbuf + inputEnd; bool escaped = false; // skip over the opening '"' - inptr++; - - using (var memory = new MemoryStream ()) { - do { - while (inptr < inend) { - if (*inptr == (byte) '"' && !escaped) - break; - - if (*inptr == (byte) '\\' && !escaped) { - escaped = true; - } else { - memory.WriteByte (*inptr); - escaped = false; - } + inputIndex++; - inptr++; - } + using (var builder = new ByteArrayBuilder (64)) { + while (!TryReadQuotedString (builder, ref escaped)) + ReadAhead (2, cancellationToken); - if (inptr + 1 < inend) { - // skip over closing '"' - inptr++; - - // Note: Some IMAP servers do not properly escape double-quotes inside - // of a qstring token and so, as an attempt at working around this - // problem, check that the closing '"' character is not immediately - // followed by any character that we would expect immediately following - // a qstring token. - // - // See https://github.com/jstedfast/MailKit/issues/485 for details. - if ("]) \r\n".IndexOf ((char) *inptr) != -1) - break; - - memory.WriteByte ((byte) '"'); - continue; - } + var qstring = builder.ToString (); - inputIndex = (int) (inptr - inbuf); + return ImapToken.Create (ImapTokenType.QString, qstring); + } + } - ReadAhead (2, cancellationToken); + async ValueTask ReadQuotedStringTokenAsync (CancellationToken cancellationToken) + { + bool escaped = false; - inptr = inbuf + inputIndex; - inend = inbuf + inputEnd; - } while (true); + // skip over the opening '"' + inputIndex++; - inputIndex = (int) (inptr - inbuf); + using (var builder = new ByteArrayBuilder (64)) { + while (!TryReadQuotedString (builder, ref escaped)) + await ReadAheadAsync (2, cancellationToken).ConfigureAwait (false); -#if !NETFX_CORE && !NETSTANDARD - var buffer = memory.GetBuffer (); -#else - var buffer = memory.ToArray (); -#endif - int length = (int) memory.Length; + var qstring = builder.ToString (); - return new ImapToken (ImapTokenType.QString, Encoding.UTF8.GetString (buffer, 0, length)); + return ImapToken.Create (ImapTokenType.QString, qstring); } } - unsafe string ReadAtomString (byte* inbuf, bool flag, string specials, CancellationToken cancellationToken) + bool TryReadAtomString (ImapTokenType type, ByteArrayBuilder builder, string specials) { - var builder = new StringBuilder (); - byte* inptr = inbuf + inputIndex; - byte* inend = inbuf + inputEnd; + input[inputEnd] = (byte) '\n'; - do { - *inend = (byte) '\n'; + if (type == ImapTokenType.Flag && builder.Length == 1 && input[inputIndex] == (byte) '*') { + // this is a special wildcard flag + builder.Append (input[inputIndex++]); + } - if (flag && builder.Length == 0 && *inptr == (byte) '*') { - // this is a special wildcard flag - inputIndex++; - return "*"; - } + while (IsAtom (input[inputIndex], specials)) + builder.Append (input[inputIndex++]); - while (IsAtom (*inptr, specials)) - builder.Append ((char) *inptr++); + return inputIndex < inputEnd; + } - if (inptr < inend) - break; + ImapToken ReadAtomString (ImapTokenType type, string specials, CancellationToken cancellationToken) + { + using (var builder = new ByteArrayBuilder (32)) { + if (type == ImapTokenType.Flag) + builder.Append ((byte) '\\'); - inputIndex = (int) (inptr - inbuf); + while (!TryReadAtomString (type, builder, specials)) + ReadAhead (1, cancellationToken); - ReadAhead (1, cancellationToken); + return ImapToken.Create (type, builder); + } + } - inptr = inbuf + inputIndex; - inend = inbuf + inputEnd; - } while (true); + async ValueTask ReadAtomStringAsync (ImapTokenType type, string specials, CancellationToken cancellationToken) + { + using (var builder = new ByteArrayBuilder (32)) { + if (type == ImapTokenType.Flag) + builder.Append ((byte) '\\'); - inputIndex = (int) (inptr - inbuf); + while (!TryReadAtomString (type, builder, specials)) + await ReadAheadAsync (1, cancellationToken).ConfigureAwait (false); - return builder.ToString (); + return ImapToken.Create (type, builder); + } } - unsafe ImapToken ReadAtomToken (byte* inbuf, string specials, CancellationToken cancellationToken) + ImapToken ReadAtomToken (string specials, CancellationToken cancellationToken) { - var atom = ReadAtomString (inbuf, false, specials, cancellationToken); + return ReadAtomString (ImapTokenType.Atom, specials, cancellationToken); + } - return atom == "NIL" ? new ImapToken (ImapTokenType.Nil, atom) : new ImapToken (ImapTokenType.Atom, atom); + ValueTask ReadAtomTokenAsync (string specials, CancellationToken cancellationToken) + { + return ReadAtomStringAsync (ImapTokenType.Atom, specials, cancellationToken); } - unsafe ImapToken ReadFlagToken (byte* inbuf, string specials, CancellationToken cancellationToken) + ImapToken ReadFlagToken (string specials, CancellationToken cancellationToken) { inputIndex++; - var flag = "\\" + ReadAtomString (inbuf, true, specials, cancellationToken); + return ReadAtomString (ImapTokenType.Flag, specials, cancellationToken); + } - return new ImapToken (ImapTokenType.Flag, flag); + ValueTask ReadFlagTokenAsync (string specials, CancellationToken cancellationToken) + { + inputIndex++; + + return ReadAtomStringAsync (ImapTokenType.Flag, specials, cancellationToken); } - unsafe ImapToken ReadLiteralToken (byte* inbuf, CancellationToken cancellationToken) + bool TryReadLiteralTokenValue (ByteArrayBuilder builder) { - var builder = new StringBuilder (); - byte* inptr = inbuf + inputIndex; - byte* inend = inbuf + inputEnd; + input[inputEnd] = (byte) '}'; - // skip over the '{' - inptr++; + while (input[inputIndex] != (byte) '}' && input[inputIndex] != '+') + builder.Append (input[inputIndex++]); - do { - *inend = (byte) '}'; + return inputIndex < inputEnd; + } - while (*inptr != (byte) '}' && *inptr != '+') - builder.Append ((char) *inptr++); + bool TryReadUntilCloseCurlyBrace (ByteArrayBuilder builder) + { + input[inputEnd] = (byte) '}'; - if (inptr < inend) - break; + while (input[inputIndex] != (byte) '}') + builder.Append (input[inputIndex++]); - inputIndex = (int) (inptr - inbuf); + return inputIndex < inputEnd; + } - ReadAhead (1, cancellationToken); + bool TrySkipUntilNewLine () + { + input[inputEnd] = (byte) '\n'; - inptr = inbuf + inputIndex; - inend = inbuf + inputEnd; - } while (true); + while (input[inputIndex] != (byte) '\n') + inputIndex++; - if (*inptr == (byte) '+') - inptr++; + return inputIndex < inputEnd; + } - // technically, we need "}\r\n", but in order to be more lenient, we'll accept "}\n" - inputIndex = (int) (inptr - inbuf); + ImapToken ReadLiteralToken (CancellationToken cancellationToken) + { + using (var builder = new ByteArrayBuilder (16)) { + // skip over the '{' + builder.Append (input[inputIndex++]); - ReadAhead (2, cancellationToken); + while (!TryReadLiteralTokenValue (builder)) + ReadAhead (1, cancellationToken); - inptr = inbuf + inputIndex; - inend = inbuf + inputEnd; + int endIndex = builder.Length; - if (*inptr != (byte) '}') { - // PROTOCOL ERROR... but maybe we can work around it? - do { - *inend = (byte) '}'; + if (input[inputIndex] == (byte) '+') + builder.Append (input[inputIndex++]); - while (*inptr != (byte) '}') - inptr++; + // technically, we need "}\r\n", but in order to be more lenient, we'll accept "}\n" + ReadAhead (2, cancellationToken); - if (inptr < inend) - break; + if (input[inputIndex] != (byte) '}') { + // PROTOCOL ERROR... but maybe we can work around it? + while (!TryReadUntilCloseCurlyBrace (builder)) + ReadAhead (1, cancellationToken); + } - inputIndex = (int) (inptr - inbuf); + // skip over the '}' + builder.Append (input[inputIndex++]); + // read until we get a new line... + while (!TrySkipUntilNewLine ()) ReadAhead (1, cancellationToken); - inptr = inbuf + inputIndex; - inend = inbuf + inputEnd; - } while (true); + // skip over the '\n' + inputIndex++; + + if (!builder.TryParse (1, endIndex, out literalDataLeft)) + return ImapToken.Create (ImapTokenType.Error, builder.ToString ()); + + Mode = ImapStreamMode.Literal; + + return ImapToken.Create (ImapTokenType.Literal, literalDataLeft); } + } - // skip over the '}' - inptr++; + async ValueTask ReadLiteralTokenAsync (CancellationToken cancellationToken) + { + using (var builder = new ByteArrayBuilder (16)) { + // skip over the '{' + builder.Append (input[inputIndex++]); - // read until we get a new line... - do { - *inend = (byte) '\n'; + while (!TryReadLiteralTokenValue (builder)) + await ReadAheadAsync (1, cancellationToken).ConfigureAwait (false); - while (*inptr != (byte) '\n') - inptr++; + int endIndex = builder.Length; - if (inptr < inend) - break; + if (input[inputIndex] == (byte) '+') + builder.Append (input[inputIndex++]); - inputIndex = (int) (inptr - inbuf); + // technically, we need "}\r\n", but in order to be more lenient, we'll accept "}\n" + await ReadAheadAsync (2, cancellationToken).ConfigureAwait (false); - ReadAhead (1, cancellationToken); + if (input[inputIndex] != (byte) '}') { + // PROTOCOL ERROR... but maybe we can work around it? + while (!TryReadUntilCloseCurlyBrace (builder)) + await ReadAheadAsync (1, cancellationToken).ConfigureAwait (false); + } - inptr = inbuf + inputIndex; - inend = inbuf + inputEnd; - *inptr = (byte) '\n'; - } while (true); + // skip over the '}' + builder.Append (input[inputIndex++]); - // skip over the '\n' - inptr++; + // read until we get a new line... + while (!TrySkipUntilNewLine ()) + await ReadAheadAsync (1, cancellationToken).ConfigureAwait (false); - inputIndex = (int) (inptr - inbuf); + // skip over the '\n' + inputIndex++; - if (!int.TryParse (builder.ToString (), out literalDataLeft) || literalDataLeft < 0) - return new ImapToken (ImapTokenType.Error, builder.ToString ()); + if (!builder.TryParse (1, endIndex, out literalDataLeft) || literalDataLeft < 0) + return ImapToken.Create (ImapTokenType.Error, builder.ToString ()); + + Mode = ImapStreamMode.Literal; + + return ImapToken.Create (ImapTokenType.Literal, literalDataLeft); + } + } + + bool TrySkipWhiteSpace () + { + input[inputEnd] = (byte) '\n'; - Mode = ImapStreamMode.Literal; + while (IsWhiteSpace (input[inputIndex])) + inputIndex++; - return new ImapToken (ImapTokenType.Literal, literalDataLeft); + return inputIndex < inputEnd; } /// /// Reads the next available token from the stream. /// /// The token. - /// A list of characters that are not legal in bare string tokens. + /// The special characters that are not allowed in an atom token. /// The cancellation token. /// /// The stream has been disposed. @@ -711,58 +825,77 @@ public ImapToken ReadToken (string specials, CancellationToken cancellationToken { CheckDisposed (); - if (nextToken != null) { - var token = nextToken; - nextToken = null; - return token; - } + if (tokens.Count > 0) + return tokens.Pop (); - unsafe { - fixed (byte* inbuf = input) { - byte* inptr = inbuf + inputIndex; - byte* inend = inbuf + inputEnd; + // skip over white space between tokens... + while (!TrySkipWhiteSpace ()) + ReadAhead (1, cancellationToken); - *inend = (byte) '\n'; + char c = (char) input[inputIndex]; - // skip over white space between tokens... - do { - while (IsWhiteSpace (*inptr)) - inptr++; + if (c == '"') + return ReadQuotedStringToken (cancellationToken); - if (inptr < inend) - break; + if (c == '{') + return ReadLiteralToken (cancellationToken); - inputIndex = (int) (inptr - inbuf); + if (c == '\\') + return ReadFlagToken (specials, cancellationToken); - ReadAhead (1, cancellationToken); + if (IsAtom (input[inputIndex], specials)) + return ReadAtomToken (specials, cancellationToken); - inptr = inbuf + inputIndex; - inend = inbuf + inputEnd; + // special character token + inputIndex++; - *inend = (byte) '\n'; - } while (true); + return ImapToken.Create ((ImapTokenType) c, c); + } - inputIndex = (int) (inptr - inbuf); - char c = (char) *inptr; + /// + /// Asynchronously reads the next available token from the stream. + /// + /// The token. + /// The special characters that are not allowed in an atom token. + /// The cancellation token. + /// + /// The stream has been disposed. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public async ValueTask ReadTokenAsync (string specials, CancellationToken cancellationToken) + { + CheckDisposed (); - if (c == '"') - return ReadQuotedStringToken (inbuf, cancellationToken); + if (tokens.Count > 0) + return tokens.Pop (); - if (c == '{') - return ReadLiteralToken (inbuf, cancellationToken); + // skip over white space between tokens... + while (!TrySkipWhiteSpace ()) + await ReadAheadAsync (1, cancellationToken).ConfigureAwait (false); - if (c == '\\') - return ReadFlagToken (inbuf, specials, cancellationToken); + char c = (char) input[inputIndex]; - if (IsAtom (*inptr, specials)) - return ReadAtomToken (inbuf, specials, cancellationToken); + if (c == '"') + return await ReadQuotedStringTokenAsync (cancellationToken).ConfigureAwait (false); - // special character token - inputIndex++; + if (c == '{') + return await ReadLiteralTokenAsync (cancellationToken).ConfigureAwait (false); - return new ImapToken ((ImapTokenType) c, c); - } - } + if (c == '\\') + return await ReadFlagTokenAsync (specials, cancellationToken).ConfigureAwait (false); + + if (IsAtom (input[inputIndex], specials)) + return await ReadAtomTokenAsync (specials, cancellationToken).ConfigureAwait (false); + + // special character token + inputIndex++; + + return ImapToken.Create ((ImapTokenType) c, c); } /// @@ -785,27 +918,81 @@ public ImapToken ReadToken (CancellationToken cancellationToken) } /// - /// Ungets a token. + /// Asynchronously reads the next available token from the stream. /// + /// The token. + /// The cancellation token. + /// + /// The stream has been disposed. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public ValueTask ReadTokenAsync (CancellationToken cancellationToken) + { + return ReadTokenAsync (DefaultSpecials, cancellationToken); + } + + /// + /// Unget a token. + /// + /// + /// Ungets a token. + /// /// The token. public void UngetToken (ImapToken token) { if (token == null) throw new ArgumentNullException (nameof (token)); - nextToken = token; + tokens.Push (token); + } + + unsafe bool TryReadLine (ByteArrayBuilder builder) + { + fixed (byte* inbuf = input) { + byte* start, inptr, inend; + int offset = inputIndex; + int count; + + start = inbuf + inputIndex; + inend = inbuf + inputEnd; + *inend = (byte) '\n'; + inptr = start; + + // FIXME: use SIMD to optimize this + while (*inptr != (byte) '\n') + inptr++; + + inputIndex = (int) (inptr - inbuf); + count = (int) (inptr - start); + + if (inptr == inend) { + builder.Append (input, offset, count); + return false; + } + + // consume the '\n' + inputIndex++; + count++; + + builder.Append (input, offset, count); + + return true; + } } /// /// Reads a single line of input from the stream. /// /// - /// This method should be called in a loop until it returns true. + /// This method should be called in a loop until it returns . /// - /// true, if reading the line is complete, false otherwise. - /// The buffer containing the line data. - /// The offset into the buffer containing bytes read. - /// The number of bytes read. + /// , if reading the line is complete, otherwise. + /// The output buffer write the line data into. /// The cancellation token. /// /// The stream has been disposed. @@ -816,41 +1003,54 @@ public void UngetToken (ImapToken token) /// /// An I/O error occurred. /// - internal bool ReadLine (out byte[] buffer, out int offset, out int count, CancellationToken cancellationToken) + internal bool ReadLine (ByteArrayBuilder builder, CancellationToken cancellationToken) { CheckDisposed (); - unsafe { - fixed (byte* inbuf = input) { - byte* start, inptr, inend; - - // we need at least 1 byte: "\n" - ReadAhead (1, cancellationToken); - - offset = inputIndex; - buffer = input; - - start = inbuf + inputIndex; - inend = inbuf + inputEnd; - *inend = (byte) '\n'; - inptr = start; + if (inputIndex == inputEnd) + ReadAhead (1, cancellationToken); - // FIXME: use SIMD to optimize this - while (*inptr != (byte) '\n') - inptr++; + return TryReadLine (builder); + } - inputIndex = (int) (inptr - inbuf); - count = (int) (inptr - start); + /// + /// Asynchronously reads a single line of input from the stream. + /// + /// + /// This method should be called in a loop until it returns . + /// + /// , if reading the line is complete, otherwise. + /// The output buffer write the line data into. + /// The cancellation token. + /// + /// The stream has been disposed. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + internal async ValueTask ReadLineAsync (ByteArrayBuilder builder, CancellationToken cancellationToken) + { + CheckDisposed (); - if (inptr == inend) - return false; + if (inputIndex == inputEnd) + await ReadAheadAsync (1, cancellationToken).ConfigureAwait (false); - // consume the '\n' - inputIndex++; - count++; + return TryReadLine (builder); + } - return true; - } + void AppendToOutputBuffer (byte[] buffer, ref int index, ref int left) + { + int n = Math.Min (BlockSize - outputIndex, left); + + if (outputIndex > 0 || n < BlockSize) { + // append the data to the output buffer + Buffer.BlockCopy (buffer, index, output, outputIndex, n); + outputIndex += n; + index += n; + left -= n; } } @@ -867,12 +1067,12 @@ internal bool ReadLine (out byte[] buffer, out int offset, out int count, Cancel /// The number of bytes to write. /// The cancellation token. /// - /// is null. + /// is . /// /// /// is less than zero or greater than the length of . /// -or- - /// The is not large enough to contain bytes strting + /// The is not large enough to contain bytes starting /// at the specified . /// /// @@ -894,24 +1094,18 @@ public void Write (byte[] buffer, int offset, int count, CancellationToken cance ValidateArguments (buffer, offset, count); try { + var network = NetworkStream.Get (Stream); int index = offset; int left = count; while (left > 0) { - int n = Math.Min (BlockSize - outputIndex, left); - - if (outputIndex > 0 || n < BlockSize) { - // append the data to the output buffer - Buffer.BlockCopy (buffer, index, output, outputIndex, n); - outputIndex += n; - index += n; - left -= n; - } + AppendToOutputBuffer (buffer, ref index, ref left); if (outputIndex == BlockSize) { // flush the output buffer - Poll (SelectMode.SelectWrite, cancellationToken); + network?.Poll (SelectMode.SelectWrite, cancellationToken); Stream.Write (output, 0, BlockSize); + logger.LogClient (output, 0, BlockSize); outputIndex = 0; } @@ -919,16 +1113,19 @@ public void Write (byte[] buffer, int offset, int count, CancellationToken cance if (outputIndex == 0) { // write blocks of data to the stream without buffering while (left >= BlockSize) { - Poll (SelectMode.SelectWrite, cancellationToken); + network?.Poll (SelectMode.SelectWrite, cancellationToken); Stream.Write (buffer, index, BlockSize); + logger.LogClient (buffer, index, BlockSize); index += BlockSize; left -= BlockSize; } } } - } catch { + } catch (Exception ex) { IsConnected = false; + if (ex is not OperationCanceledException) + cancellationToken.ThrowIfCancellationRequested (); throw; } } @@ -945,12 +1142,12 @@ public void Write (byte[] buffer, int offset, int count, CancellationToken cance /// The offset of the first byte to write. /// The number of bytes to write. /// - /// is null. + /// is . /// /// /// is less than zero or greater than the length of . /// -or- - /// The is not large enough to contain bytes strting + /// The is not large enough to contain bytes starting /// at the specified . /// /// @@ -967,6 +1164,78 @@ public override void Write (byte[] buffer, int offset, int count) Write (buffer, offset, count, CancellationToken.None); } + /// + /// Writes a sequence of bytes to the stream and advances the current + /// position within this stream by the number of bytes written. + /// + /// + /// Writes a sequence of bytes to the stream and advances the current + /// position within this stream by the number of bytes written. + /// + /// A task that represents the asynchronous write operation. + /// The buffer to write. + /// The offset of the first byte to write. + /// The number of bytes to write. + /// The cancellation token. + /// + /// is . + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes starting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support writing. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public override async Task WriteAsync (byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + CheckDisposed (); + + ValidateArguments (buffer, offset, count); + + try { + int index = offset; + int left = count; + + while (left > 0) { + AppendToOutputBuffer (buffer, ref index, ref left); + + if (outputIndex == BlockSize) { + // flush the output buffer + await Stream.WriteAsync (output, 0, BlockSize, cancellationToken).ConfigureAwait (false); + logger.LogClient (output, 0, BlockSize); + outputIndex = 0; + } + + if (outputIndex == 0) { + // write blocks of data to the stream without buffering + while (left >= BlockSize) { + await Stream.WriteAsync (buffer, index, BlockSize, cancellationToken).ConfigureAwait (false); + logger.LogClient (buffer, index, BlockSize); + index += BlockSize; + left -= BlockSize; + } + } + } + } catch (Exception ex) { + IsConnected = false; + if (ex is not OperationCanceledException) + cancellationToken.ThrowIfCancellationRequested (); + throw; + } + } + /// /// Clears all output buffers for this stream and causes any buffered data to be written /// to the underlying device. @@ -996,13 +1265,18 @@ public void Flush (CancellationToken cancellationToken) return; try { - Poll (SelectMode.SelectWrite, cancellationToken); + var network = NetworkStream.Get (Stream); + + network?.Poll (SelectMode.SelectWrite, cancellationToken); Stream.Write (output, 0, outputIndex); Stream.Flush (); + logger.LogClient (output, 0, outputIndex); outputIndex = 0; - } catch { + } catch (Exception ex) { IsConnected = false; + if (ex is not OperationCanceledException) + cancellationToken.ThrowIfCancellationRequested (); throw; } } @@ -1029,6 +1303,49 @@ public override void Flush () Flush (CancellationToken.None); } + /// + /// Clears all buffers for this stream and causes any buffered data to be written + /// to the underlying device. + /// + /// + /// Clears all buffers for this stream and causes any buffered data to be written + /// to the underlying device. + /// + /// A task that represents the asynchronous flush operation. + /// The cancellation token. + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support writing. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public override async Task FlushAsync (CancellationToken cancellationToken) + { + CheckDisposed (); + + if (outputIndex == 0) + return; + + try { + await Stream.WriteAsync (output, 0, outputIndex, cancellationToken).ConfigureAwait (false); + await Stream.FlushAsync (cancellationToken).ConfigureAwait (false); + + logger.LogClient (output, 0, outputIndex); + outputIndex = 0; + } catch (Exception ex) { + IsConnected = false; + if (ex is not OperationCanceledException) + cancellationToken.ThrowIfCancellationRequested (); + throw; + } + } + /// /// Sets the position within the current stream. /// @@ -1069,8 +1386,8 @@ public override void SetLength (long value) /// Releases the unmanaged resources used by the and /// optionally releases the managed resources. /// - /// true to release both managed and unmanaged resources; - /// false to release only the unmanaged resources. + /// to release both managed and unmanaged resources; + /// to release only the unmanaged resources. protected override void Dispose (bool disposing) { if (disposing && !disposed) { diff --git a/MailKit/Net/Imap/ImapToken.cs b/MailKit/Net/Imap/ImapToken.cs index 4c8ec41645..ceda1a3e7b 100644 --- a/MailKit/Net/Imap/ImapToken.cs +++ b/MailKit/Net/Imap/ImapToken.cs @@ -1,9 +1,9 @@ -// +// // ImapToken.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -24,6 +24,11 @@ // THE SOFTWARE. // +using System.Globalization; +using System.Collections.Generic; + +using MimeKit.Utils; + namespace MailKit.Net.Imap { enum ImapTokenType { NoData = -7, @@ -45,10 +50,53 @@ enum ImapTokenType { class ImapToken { + public static readonly ImapToken Plus = new ImapToken (ImapTokenType.Atom, "+"); + public static readonly ImapToken Asterisk = new ImapToken (ImapTokenType.Asterisk, '*'); + public static readonly ImapToken OpenParen = new ImapToken (ImapTokenType.OpenParen, '('); + public static readonly ImapToken CloseParen = new ImapToken (ImapTokenType.CloseParen, ')'); + public static readonly ImapToken OpenBracket = new ImapToken (ImapTokenType.OpenBracket, '['); + public static readonly ImapToken CloseBracket = new ImapToken (ImapTokenType.CloseBracket, ']'); + public static readonly ImapToken Nil = new ImapToken (ImapTokenType.Nil, "NIL"); + public static readonly ImapToken Eoln = new ImapToken (ImapTokenType.Eoln, '\n'); + + static readonly ImapToken[] CommonMessageFlagTokens = new ImapToken[] { + new ImapToken (ImapTokenType.Flag, "\\Answered"), + new ImapToken (ImapTokenType.Flag, "\\Deleted"), + new ImapToken (ImapTokenType.Flag, "\\Draft"), + new ImapToken (ImapTokenType.Flag, "\\Flagged"), + new ImapToken (ImapTokenType.Flag, "\\Recent"), + new ImapToken (ImapTokenType.Flag, "\\Seen"), + new ImapToken (ImapTokenType.Flag, "\\*") + }; + + static readonly List NilTokens = new List (6) { + Nil + }; + + static readonly ImapToken Ok = new ImapToken (ImapTokenType.Atom, "OK"); + static readonly ImapToken Fetch = new ImapToken (ImapTokenType.Atom, "FETCH"); + //static readonly ImapToken Annotation = new ImapToken (ImapTokenType.Atom, "ANNOTATION"); + static readonly ImapToken Body = new ImapToken (ImapTokenType.Atom, "BODY"); + static readonly ImapToken BodyStructure = new ImapToken (ImapTokenType.Atom, "BODYSTRUCTURE"); + //static readonly ImapToken EmailId = new ImapToken (ImapTokenType.Atom, "EMAILID"); + static readonly ImapToken Envelope = new ImapToken (ImapTokenType.Atom, "ENVELOPE"); + static readonly ImapToken Flags = new ImapToken (ImapTokenType.Atom, "FLAGS"); + //static readonly ImapToken Header = new ImapToken (ImapTokenType.Atom, "HEADER"); + //static readonly ImapToken HeaderFields = new ImapToken (ImapTokenType.Atom, "HEADER.FIELDS"); + static readonly ImapToken InternalDate = new ImapToken (ImapTokenType.Atom, "INTERNALDATE"); + static readonly ImapToken ModSeq = new ImapToken (ImapTokenType.Atom, "MODSEQ"); + static readonly ImapToken Rfc822Size = new ImapToken (ImapTokenType.Atom, "RFC822.SIZE"); + //static readonly ImapToken SaveDate = new ImapToken (ImapTokenType.Atom, "SAVEDATE"); + //static readonly ImapToken ThreadId = new ImapToken (ImapTokenType.Atom, "THREADID"); + static readonly ImapToken Uid = new ImapToken (ImapTokenType.Atom, "UID"); + static readonly ImapToken XGMLabels = new ImapToken (ImapTokenType.Atom, "X-GM-LABELS"); + static readonly ImapToken XGMMsgId = new ImapToken (ImapTokenType.Atom, "X-GM-MSGID"); + static readonly ImapToken XGMThrId = new ImapToken (ImapTokenType.Atom, "X-GM-THRID"); + public readonly ImapTokenType Type; public readonly object Value; - public ImapToken (ImapTokenType type, object value = null) + ImapToken (ImapTokenType type, object value) { Value = value; Type = type; @@ -56,22 +104,111 @@ public ImapToken (ImapTokenType type, object value = null) //System.Console.WriteLine ("token: {0}", this); } + public static ImapToken Create (ImapTokenType type, char c) + { + switch (type) { + case ImapTokenType.Asterisk: return Asterisk; + case ImapTokenType.OpenParen: return OpenParen; + case ImapTokenType.CloseParen: return CloseParen; + case ImapTokenType.OpenBracket: return OpenBracket; + case ImapTokenType.CloseBracket: return CloseBracket; + case ImapTokenType.Eoln: return Eoln; + } + + return new ImapToken (type, c); + } + + public static ImapToken Create (ImapTokenType type, int literalLength) + { + return new ImapToken (type, literalLength); + } + + public static ImapToken Create (ImapTokenType type, ByteArrayBuilder builder) + { + string value; + + if (type == ImapTokenType.Flag) { + foreach (var token in CommonMessageFlagTokens) { + value = (string) token.Value; + + if (builder.Equals (value, true)) + return token; + } + } else if (type == ImapTokenType.Atom) { + if (builder.Equals ("NIL", true)) { + // Look for the cached NIL token that matches this capitalization. + lock (NilTokens) { + foreach (var token in NilTokens) { + value = (string) token.Value; + + if (builder.Equals (value)) + return token; + } + + // Add this new variation to our NIL token cache. + var nil = new ImapToken (ImapTokenType.Nil, builder.ToString ()); + NilTokens.Add (nil); + + return nil; + } + } + + if (builder.Equals ("+", false)) + return Plus; + if (builder.Equals ("OK", false)) + return Ok; + if (builder.Equals ("FETCH", false)) + return Fetch; + if (builder.Equals ("BODY", false)) + return Body; + if (builder.Equals ("BODYSTRUCTURE", false)) + return BodyStructure; + if (builder.Equals ("ENVELOPE", false)) + return Envelope; + if (builder.Equals ("FLAGS", false)) + return Flags; + if (builder.Equals ("INTERNALDATE", false)) + return InternalDate; + if (builder.Equals ("MODSEQ", false)) + return ModSeq; + if (builder.Equals ("RFC822.SIZE", false)) + return Rfc822Size; + if (builder.Equals ("UID", false)) + return Uid; + if (builder.Equals ("X-GM-LABELS", false)) + return XGMLabels; + if (builder.Equals ("X-GM-MSGID", false)) + return XGMMsgId; + if (builder.Equals ("X-GM-THRID", false)) + return XGMThrId; + } + + value = builder.ToString (); + + return new ImapToken (type, value); + } + + public static ImapToken Create (ImapTokenType type, string value) + { + return new ImapToken (type, value); + } + public override string ToString () { switch (Type) { case ImapTokenType.NoData: return ""; - case ImapTokenType.Nil: return "NIL"; - case ImapTokenType.Atom: return "[atom: " + (string) Value + "]"; - case ImapTokenType.Flag: return "[flag: " + (string) Value + "]"; - case ImapTokenType.QString: return "[qstring: \"" + (string) Value + "\"]"; - case ImapTokenType.Literal: return "{" + (int) Value + "}"; + case ImapTokenType.Nil: return (string) Value; + case ImapTokenType.Atom: return (string) Value; + case ImapTokenType.Flag: return (string) Value; + case ImapTokenType.QString: return MimeUtils.Quote ((string) Value); + case ImapTokenType.Literal: return string.Format (CultureInfo.InvariantCulture, "{{{0}}}", (int) Value); case ImapTokenType.Eoln: return "'\\n'"; case ImapTokenType.OpenParen: return "'('"; case ImapTokenType.CloseParen: return "')'"; case ImapTokenType.Asterisk: return "'*'"; case ImapTokenType.OpenBracket: return "'['"; case ImapTokenType.CloseBracket: return "']'"; - default: return string.Format ("[{0}: '{1}']", Type, Value); + default: return string.Format (CultureInfo.InvariantCulture, "[{0}: '{1}']", Type, Value); } } } diff --git a/MailKit/Net/Imap/ImapUtils.cs b/MailKit/Net/Imap/ImapUtils.cs index 3c466aa58f..b90cab5608 100644 --- a/MailKit/Net/Imap/ImapUtils.cs +++ b/MailKit/Net/Imap/ImapUtils.cs @@ -1,9 +1,9 @@ -// +// // ImapUtils.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -30,24 +30,25 @@ using System.Threading; using System.Diagnostics; using System.Globalization; +using System.Threading.Tasks; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; using MimeKit; using MimeKit.Utils; -#if NETFX_CORE -using Encoding = Portable.Text.Encoding; -#endif - namespace MailKit.Net.Imap { /// /// IMAP utility functions. /// static class ImapUtils { + const FolderAttributes SpecialUseAttributes = FolderAttributes.All | FolderAttributes.Archive | FolderAttributes.Drafts | + FolderAttributes.Flagged | FolderAttributes.Important | FolderAttributes.Inbox | FolderAttributes.Junk | + FolderAttributes.Sent | FolderAttributes.Trash; const string QuotedSpecials = " \t()<>@,;:\\\"/[]?="; - static int InboxLength = "INBOX".Length; + static readonly int InboxLength = "INBOX".Length; static readonly string[] Months = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", @@ -61,9 +62,9 @@ static class ImapUtils /// The date. public static string FormatInternalDate (DateTimeOffset date) { - return string.Format ("{0:D2}-{1}-{2:D4} {3:D2}:{4:D2}:{5:D2} {6:+00;-00}{7:00}", + return string.Format (CultureInfo.InvariantCulture, "{0:D2}-{1}-{2:D4} {3:D2}:{4:D2}:{5:D2} {6:+00;-00}{7:00}", date.Day, Months[date.Month - 1], date.Year, date.Hour, date.Minute, date.Second, - date.Offset.Hours, date.Offset.Minutes); + date.Offset.Hours, Math.Abs (date.Offset.Minutes)); } static bool TryGetInt32 (string text, ref int index, out int value) @@ -75,12 +76,7 @@ static bool TryGetInt32 (string text, ref int index, out int value) while (index < text.Length && text[index] >= '0' && text[index] <= '9') { int digit = text[index] - '0'; - if (value > int.MaxValue / 10) { - // integer overflow - return false; - } - - if (value == int.MaxValue / 10 && digit > int.MaxValue % 10) { + if (value > int.MaxValue / 10 || (value == int.MaxValue / 10 && digit > int.MaxValue % 10)) { // integer overflow return false; } @@ -118,12 +114,7 @@ static bool TryGetMonth (string text, ref int index, char delim, out int month) static bool TryGetTimeZone (string text, ref int index, out TimeSpan timezone) { - int tzone, sign = 1; - - if (index >= text.Length) { - timezone = new TimeSpan (); - return false; - } + int sign = 1; if (text[index] == '-') { sign = -1; @@ -132,7 +123,7 @@ static bool TryGetTimeZone (string text, ref int index, out TimeSpan timezone) index++; } - if (!TryGetInt32 (text, ref index, out tzone)) { + if (!TryGetInt32 (text, ref index, out var tzone)) { timezone = new TimeSpan (); return false; } @@ -153,11 +144,6 @@ static bool TryGetTimeZone (string text, ref int index, out TimeSpan timezone) return true; } - static Exception InvalidInternalDateFormat (string text) - { - return new FormatException ("Invalid INTERNALDATE format: " + text); - } - /// /// Parses the internal date string. /// @@ -165,70 +151,128 @@ static Exception InvalidInternalDateFormat (string text) /// The text to parse. public static DateTimeOffset ParseInternalDate (string text) { - int day, month, year, hour, minute, second; - TimeSpan timezone; int index = 0; while (index < text.Length && char.IsWhiteSpace (text[index])) index++; - if (index >= text.Length || !TryGetInt32 (text, ref index, '-', out day) || day < 1 || day > 31) - throw InvalidInternalDateFormat (text); + if (index >= text.Length || !TryGetInt32 (text, ref index, '-', out int day) || day < 1 || day > 31) + return DateTimeOffset.MinValue; index++; - if (index >= text.Length || !TryGetMonth (text, ref index, '-', out month)) - throw InvalidInternalDateFormat (text); + if (index >= text.Length || !TryGetMonth (text, ref index, '-', out int month)) + return DateTimeOffset.MinValue; index++; - if (index >= text.Length || !TryGetInt32 (text, ref index, ' ', out year) || year < 1969) - throw InvalidInternalDateFormat (text); + if (index >= text.Length || !TryGetInt32 (text, ref index, ' ', out int year) || year < 1969) + return DateTimeOffset.MinValue; index++; - if (index >= text.Length || !TryGetInt32 (text, ref index, ':', out hour) || hour > 23) - throw InvalidInternalDateFormat (text); + if (index >= text.Length || !TryGetInt32 (text, ref index, ':', out int hour) || hour > 23) + return DateTimeOffset.MinValue; index++; - if (index >= text.Length || !TryGetInt32 (text, ref index, ':', out minute) || minute > 59) - throw InvalidInternalDateFormat (text); + if (index >= text.Length || !TryGetInt32 (text, ref index, ':', out int minute) || minute > 59) + return DateTimeOffset.MinValue; index++; - if (index >= text.Length || !TryGetInt32 (text, ref index, ' ', out second) || second > 59) - throw InvalidInternalDateFormat (text); + if (index >= text.Length || !TryGetInt32 (text, ref index, ' ', out int second) || second > 59) + return DateTimeOffset.MinValue; index++; - if (index >= text.Length || !TryGetTimeZone (text, ref index, out timezone)) - throw InvalidInternalDateFormat (text); + if (index >= text.Length || !TryGetTimeZone (text, ref index, out var timezone)) + return DateTimeOffset.MinValue; while (index < text.Length && char.IsWhiteSpace (text[index])) index++; if (index < text.Length) - throw InvalidInternalDateFormat (text); + return DateTimeOffset.MinValue; // return DateTimeOffset.ParseExact (text.Trim (), "d-MMM-yyyy HH:mm:ss zzz", CultureInfo.InvariantCulture.DateTimeFormat); return new DateTimeOffset (year, month, day, hour, minute, second, timezone); } + /// + /// Formats a list of annotations for a STORE or APPEND command. + /// + /// The command builder. + /// The annotations. + /// the argument list. + /// Throw an exception if there are any annotations without properties. + public static void FormatAnnotations (StringBuilder command, IList annotations, List args, bool throwOnError) + { + int length = command.Length; + int added = 0; + + command.Append ("ANNOTATION ("); + + for (int i = 0; i < annotations.Count; i++) { + var annotation = annotations[i]; + + if (annotation.Properties.Count == 0) { + if (throwOnError) + throw new ArgumentException ("One or more annotations does not define any attributes.", nameof (annotations)); + + continue; + } + + command.Append (annotation.Entry); + command.Append (" ("); + + foreach (var property in annotation.Properties) { + command.Append (property.Key); + + if (property.Value != null) { + command.Append (" %S "); + args.Add (property.Value); + } else { + command.Append (" NIL "); + } + } + + command[command.Length - 1] = ')'; + command.Append (' '); + + added++; + } + + if (added > 0) + command[command.Length - 1] = ')'; + else + command.Length = length; + } + /// /// Formats the array of indexes as a string suitable for use with IMAP commands. /// - /// The index set. + /// The IMAP engine. + /// The string builder. /// The indexes. /// - /// is null. + /// is . + /// -or- + /// is . + /// -or- + /// is . /// /// /// One or more of the indexes has a negative value. /// - public static string FormatIndexSet (IList indexes) + public static void FormatIndexSet (ImapEngine engine, StringBuilder builder, IList indexes) { + if (engine == null) + throw new ArgumentNullException (nameof (engine)); + + if (builder == null) + throw new ArgumentNullException (nameof (builder)); + if (indexes == null) throw new ArgumentNullException (nameof (indexes)); if (indexes.Count == 0) throw new ArgumentException ("No indexes were specified.", nameof (indexes)); - var builder = new StringBuilder (); int index = 0; while (index < indexes.Count) { @@ -247,7 +291,7 @@ public static string FormatIndexSet (IList indexes) end++; i++; } - } else if (indexes[i] == end - 1) { + } else if (indexes[i] == end - 1 && engine.QuirksMode != ImapQuirksMode.hMailServer) { end = indexes[i++]; while (i < indexes.Count && indexes[i] == end - 1) { @@ -257,40 +301,41 @@ public static string FormatIndexSet (IList indexes) } } - if (builder.Length > 0) + if (index > 0) builder.Append (','); - if (begin != end) - builder.AppendFormat ("{0}:{1}", begin + 1, end + 1); - else - builder.Append ((begin + 1).ToString ()); + builder.Append ((begin + 1).ToString (CultureInfo.InvariantCulture)); + + if (begin != end) { + builder.Append (':'); + builder.Append ((end + 1).ToString (CultureInfo.InvariantCulture)); + } index = i; } - - return builder.ToString (); } /// - /// Formats the array of UIDs as a string suitable for use with IMAP commands. + /// Formats the array of indexes as a string suitable for use with IMAP commands. /// - /// The UID set. - /// The UIDs. + /// The index set. + /// The IMAP engine. + /// The indexes. /// - /// is null. + /// is . + /// -or- + /// is . /// - /// - /// One or more of the UIDs is invalid. + /// + /// One or more of the indexes has a negative value. /// - public static string FormatUidSet (IList uids) + public static string FormatIndexSet (ImapEngine engine, IList indexes) { - if (uids == null) - throw new ArgumentNullException (nameof (uids)); + var builder = new StringBuilder (); - if (uids.Count == 0) - throw new ArgumentException ("No uids were specified.", nameof (uids)); + FormatIndexSet (engine, builder, indexes); - return UniqueIdSet.ToString (uids); + return builder.ToString (); } /// @@ -298,18 +343,16 @@ public static string FormatUidSet (IList uids) /// /// The IMAP engine. /// The IMAP command. - /// The index. - public static void ParseImplementation (ImapEngine engine, ImapCommand ic, int index) + static void ParseImplementation (ImapEngine engine, ImapCommand ic) { - string format = string.Format (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ID", "{0}"); + var format = string.Format (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ID", "{0}"); var token = engine.ReadToken (ic.CancellationToken); ImapImplementation implementation; if (token.Type == ImapTokenType.Nil) return; - if (token.Type != ImapTokenType.OpenParen) - throw ImapEngine.UnexpectedToken (format, token); + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, format, token); token = engine.PeekToken (ic.CancellationToken); @@ -330,6 +373,59 @@ public static void ParseImplementation (ImapEngine engine, ImapCommand ic, int i engine.ReadToken (ic.CancellationToken); } + /// + /// Asynchronously parses an untagged ID response. + /// + /// The IMAP engine. + /// The IMAP command. + static async Task ParseImplementationAsync (ImapEngine engine, ImapCommand ic) + { + var format = string.Format (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ID", "{0}"); + var token = await engine.ReadTokenAsync (ic.CancellationToken).ConfigureAwait (false); + ImapImplementation implementation; + + if (token.Type == ImapTokenType.Nil) + return; + + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, format, token); + + token = await engine.PeekTokenAsync (ic.CancellationToken).ConfigureAwait (false); + + implementation = new ImapImplementation (); + + while (token.Type != ImapTokenType.CloseParen) { + var property = await ReadStringTokenAsync (engine, format, ic.CancellationToken).ConfigureAwait (false); + var value = await ReadNStringTokenAsync (engine, format, false, ic.CancellationToken).ConfigureAwait (false); + + implementation.Properties[property] = value; + + token = await engine.PeekTokenAsync (ic.CancellationToken).ConfigureAwait (false); + } + + ic.UserData = implementation; + + // read the ')' token + await engine.ReadTokenAsync (ic.CancellationToken).ConfigureAwait (false); + } + + /// + /// Handles an untagged ID response. + /// + /// An asynchronous task. + /// The IMAP engine. + /// The IMAP command. + /// The index. + /// Whether or not asynchronous IO methods should be used. + public static Task UntaggedIdHandler (ImapEngine engine, ImapCommand ic, int index, bool doAsync) + { + if (doAsync) + return ParseImplementationAsync (engine, ic); + + ParseImplementation (engine, ic); + + return Task.CompletedTask; + } + /// /// Canonicalize the name of the mailbox. /// @@ -357,7 +453,7 @@ public static string CanonicalizeMailboxName (string mailboxName, char directory /// /// Determines whether the specified mailbox is the Inbox. /// - /// true if the specified mailbox name is the Inbox; otherwise, false. + /// if the specified mailbox name is the Inbox; otherwise, . /// The mailbox name. public static bool IsInbox (string mailboxName) { @@ -365,325 +461,910 @@ public static bool IsInbox (string mailboxName) } /// - /// Parses an untagged LIST or LSUB response. + /// Reads a folder name. /// - /// The IMAP engine. - /// The IMAP command. - /// The index. - public static void ParseFolderList (ImapEngine engine, ImapCommand ic, int index) + /// The IMAP engine + /// The exception format string. + /// Whether or not this is a LIST (or LSUB) response. + /// The cancellation token. + /// The folder name. + public static string ReadFolderName (ImapEngine engine, string format, bool isList, CancellationToken cancellationToken) { - string format = string.Format (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "LIST", "{0}"); - var token = engine.ReadToken (ic.CancellationToken); - var list = (List) ic.UserData; - var attrs = FolderAttributes.None; + var token = engine.ReadToken (ImapStream.AtomSpecials, cancellationToken); string encodedName; - ImapFolder folder; - char delim; - // parse the folder attributes list - if (token.Type != ImapTokenType.OpenParen) - throw ImapEngine.UnexpectedToken (format, token); + switch (token.Type) { + case ImapTokenType.Literal: + encodedName = engine.ReadLiteral (cancellationToken); + break; + case ImapTokenType.QString: + case ImapTokenType.Atom: + encodedName = (string) token.Value; - token = engine.ReadToken (ic.CancellationToken); + if (isList) { + // Note: Exchange (Office365 and potentially 2016/2019/other versions) has a bug where it doesn't quote folder names that contain CTRL characters (including tab). + // + // See https://github.com/jstedfast/MailKit/issues/945 for details. + if (token.Type == ImapTokenType.Atom && engine.QuirksMode == ImapQuirksMode.Exchange) { + var line = engine.ReadLine (cancellationToken); - while (token.Type == ImapTokenType.Flag || token.Type == ImapTokenType.Atom) { - var atom = (string) token.Value; + // unget the \r\n sequence + engine.UngetToken (ImapToken.Eoln); - switch (atom) { - case "\\NoInferiors": attrs |= FolderAttributes.NoInferiors; break; - case "\\Noselect": attrs |= FolderAttributes.NoSelect; break; - case "\\Marked": attrs |= FolderAttributes.Marked; break; - case "\\Unmarked": attrs |= FolderAttributes.Unmarked; break; - case "\\NonExistent": attrs |= FolderAttributes.NonExistent; break; - case "\\Subscribed": attrs |= FolderAttributes.Subscribed; break; - case "\\Remote": attrs |= FolderAttributes.Remote; break; - case "\\HasChildren": attrs |= FolderAttributes.HasChildren; break; - case "\\HasNoChildren": attrs |= FolderAttributes.HasNoChildren; break; - case "\\All": attrs |= FolderAttributes.All; break; - case "\\Archive": attrs |= FolderAttributes.Archive; break; - case "\\Drafts": attrs |= FolderAttributes.Drafts; break; - case "\\Flagged": attrs |= FolderAttributes.Flagged; break; - case "\\Junk": attrs |= FolderAttributes.Junk; break; - case "\\Sent": attrs |= FolderAttributes.Sent; break; - case "\\Trash": attrs |= FolderAttributes.Trash; break; - // XLIST flags: - case "\\AllMail": attrs |= FolderAttributes.All; break; - case "\\Important": attrs |= FolderAttributes.Flagged; break; - case "\\Inbox": attrs |= FolderAttributes.Inbox; break; - case "\\Spam": attrs |= FolderAttributes.Junk; break; - case "\\Starred": attrs |= FolderAttributes.Flagged; break; + encodedName += line; + } } - - token = engine.ReadToken (ic.CancellationToken); - } - - if (token.Type != ImapTokenType.CloseParen) - throw ImapEngine.UnexpectedToken (format, token); - - // parse the path delimeter - token = engine.ReadToken (ic.CancellationToken); - - if (token.Type == ImapTokenType.QString) { - var qstring = (string) token.Value; - - delim = qstring[0]; - } else if (token.Type == ImapTokenType.Nil) { - delim = '\0'; - } else { + break; + case ImapTokenType.Nil: + // Note: according to rfc3501, section 4.5, NIL is acceptable as a mailbox name. + return "NIL"; + default: throw ImapEngine.UnexpectedToken (format, token); } - // parse the folder name - token = engine.ReadToken (ImapStream.AtomSpecials, ic.CancellationToken); + return encodedName; + } + + /// + /// Asynchronously reads a folder name. + /// + /// The IMAP engine + /// The exception format string. + /// Whether or not this is a LIST (or LSUB) response. + /// The cancellation token. + /// The folder name. + public static async Task ReadFolderNameAsync (ImapEngine engine, string format, bool isList, CancellationToken cancellationToken) + { + var token = await engine.ReadTokenAsync (ImapStream.AtomSpecials, cancellationToken).ConfigureAwait (false); + string encodedName; switch (token.Type) { case ImapTokenType.Literal: - encodedName = engine.ReadLiteral (ic.CancellationToken); + encodedName = await engine.ReadLiteralAsync (cancellationToken).ConfigureAwait (false); break; case ImapTokenType.QString: case ImapTokenType.Atom: encodedName = (string) token.Value; + + if (isList) { + // Note: Exchange (Office365 and potentially 2016/2019/other versions) has a bug where it doesn't quote folder names that contain CTRL characters (including tab). + // + // See https://github.com/jstedfast/MailKit/issues/945 for details. + if (token.Type == ImapTokenType.Atom && engine.QuirksMode == ImapQuirksMode.Exchange) { + var line = await engine.ReadLineAsync (cancellationToken).ConfigureAwait (false); + + // unget the \r\n sequence + engine.UngetToken (ImapToken.Eoln); + + encodedName += line; + } + } break; case ImapTokenType.Nil: // Note: according to rfc3501, section 4.5, NIL is acceptable as a mailbox name. - encodedName = "NIL"; - break; + return "NIL"; default: throw ImapEngine.UnexpectedToken (format, token); } - if (IsInbox (encodedName)) - attrs |= FolderAttributes.Inbox; - - if (engine.GetCachedFolder (encodedName, out folder)) { - attrs |= (folder.Attributes & ~(FolderAttributes.Marked | FolderAttributes.Unmarked)); - folder.UpdateAttributes (attrs); - } else { - folder = engine.CreateImapFolder (encodedName, attrs, delim); - engine.CacheFolder (folder); - } - - list.Add (folder); + return encodedName; } - /// - /// Parses an untagged METADATA response. - /// - /// The IMAP engine. - /// The IMAP command. - /// The index. - public static void ParseMetadata (ImapEngine engine, ImapCommand ic, int index) + static void AddFolderAttribute (ref FolderAttributes attrs, string atom) { - string format = string.Format (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "METADATA", "{0}"); - var encodedName = ReadStringToken (engine, format, ic.CancellationToken); - var metadata = (MetadataCollection)ic.UserData; - ImapFolder folder; - - engine.GetCachedFolder (encodedName, out folder); + if (atom.Equals ("\\noinferiors", StringComparison.OrdinalIgnoreCase)) + attrs |= FolderAttributes.NoInferiors; + else if (atom.Equals ("\\noselect", StringComparison.OrdinalIgnoreCase)) + attrs |= FolderAttributes.NoSelect; + else if (atom.Equals ("\\marked", StringComparison.OrdinalIgnoreCase)) + attrs |= FolderAttributes.Marked; + else if (atom.Equals ("\\unmarked", StringComparison.OrdinalIgnoreCase)) + attrs |= FolderAttributes.Unmarked; + else if (atom.Equals ("\\nonexistent", StringComparison.OrdinalIgnoreCase)) + attrs |= FolderAttributes.NonExistent; + else if (atom.Equals ("\\subscribed", StringComparison.OrdinalIgnoreCase)) + attrs |= FolderAttributes.Subscribed; + else if (atom.Equals ("\\remote", StringComparison.OrdinalIgnoreCase)) + attrs |= FolderAttributes.Remote; + else if (atom.Equals ("\\haschildren", StringComparison.OrdinalIgnoreCase)) + attrs |= FolderAttributes.HasChildren; + else if (atom.Equals ("\\hasnochildren", StringComparison.OrdinalIgnoreCase)) + attrs |= FolderAttributes.HasNoChildren; + else if (atom.Equals ("\\all", StringComparison.OrdinalIgnoreCase)) + attrs |= FolderAttributes.All; + else if (atom.Equals ("\\archive", StringComparison.OrdinalIgnoreCase)) + attrs |= FolderAttributes.Archive; + else if (atom.Equals ("\\drafts", StringComparison.OrdinalIgnoreCase)) + attrs |= FolderAttributes.Drafts; + else if (atom.Equals ("\\flagged", StringComparison.OrdinalIgnoreCase)) + attrs |= FolderAttributes.Flagged; + else if (atom.Equals ("\\important", StringComparison.OrdinalIgnoreCase)) + attrs |= FolderAttributes.Important; + else if (atom.Equals ("\\junk", StringComparison.OrdinalIgnoreCase)) + attrs |= FolderAttributes.Junk; + else if (atom.Equals ("\\sent", StringComparison.OrdinalIgnoreCase)) + attrs |= FolderAttributes.Sent; + else if (atom.Equals ("\\trash", StringComparison.OrdinalIgnoreCase)) + attrs |= FolderAttributes.Trash; + // XLIST flags: + else if (atom.Equals ("\\allmail", StringComparison.OrdinalIgnoreCase)) + attrs |= FolderAttributes.All; + else if (atom.Equals ("\\inbox", StringComparison.OrdinalIgnoreCase)) + attrs |= FolderAttributes.Inbox; + else if (atom.Equals ("\\spam", StringComparison.OrdinalIgnoreCase)) + attrs |= FolderAttributes.Junk; + else if (atom.Equals ("\\starred", StringComparison.OrdinalIgnoreCase)) + attrs |= FolderAttributes.Flagged; + } - var token = engine.ReadToken (ic.CancellationToken); + static void AddFolder (ImapEngine engine, List? list, ImapFolder? folder, string encodedName, char delim, FolderAttributes attrs, bool isLsub, bool returnsSubscribed) + { + if (folder != null || engine.TryGetCachedFolder (encodedName, out folder)) { + if ((attrs & FolderAttributes.NonExistent) != 0) { + folder.UnsetPermanentFlags (); + folder.UnsetAcceptedFlags (); + folder.UpdateUidNext (UniqueId.Invalid); + folder.UpdateHighestModSeq (0); + folder.UpdateUidValidity (0); + folder.UpdateUnread (0); + } - if (token.Type != ImapTokenType.OpenParen) - throw ImapEngine.UnexpectedToken (format, token); + if (isLsub) { + // Note: merge all pre-existing attributes since the LSUB response will not contain them + attrs |= folder.Attributes | FolderAttributes.Subscribed; + } else { + // Note: only merge the SPECIAL-USE and \Subscribed attributes for a LIST command + attrs |= folder.Attributes & SpecialUseAttributes; - while (token.Type != ImapTokenType.CloseParen) { - var tag = ReadStringToken (engine, format, ic.CancellationToken); - var value = ReadStringToken (engine, format, ic.CancellationToken); + // Note: only merge \Subscribed if the LIST command isn't expected to include it + if (!returnsSubscribed) + attrs |= folder.Attributes & FolderAttributes.Subscribed; + } - metadata.Add (new Metadata (MetadataTag.Create (tag), value)); + folder.UpdateAttributes (attrs); + } else { + folder = engine.CreateImapFolder (encodedName, attrs, delim); + engine.CacheFolder (folder); - token = engine.PeekToken (ic.CancellationToken); + if (list == null) + engine.OnFolderCreated (folder); } - // read the closing paren - engine.ReadToken (ic.CancellationToken); + // Note: list will be null if this is an unsolicited LIST response due to an active NOTIFY request + list?.Add (folder); } - static string ReadStringToken (ImapEngine engine, string format, CancellationToken cancellationToken) + static void ProcessListExtensionProperty (ImapEngine engine, ref ImapFolder? folder, string encodedName, char delim, FolderAttributes attrs, string property, string? value) { - var token = engine.ReadToken (cancellationToken); - - switch (token.Type) { - case ImapTokenType.Literal: - return engine.ReadLiteral (cancellationToken); - case ImapTokenType.QString: - case ImapTokenType.Atom: - return (string) token.Value; - default: - throw ImapEngine.UnexpectedToken (format, token); + if (property.Equals ("OLDNAME", StringComparison.OrdinalIgnoreCase) && value != null) { + var oldEncodedName = value.TrimEnd (delim); + + if (engine.FolderCache.TryGetValue (oldEncodedName, out ImapFolder? oldFolder)) { + engine.FolderCache.Remove (oldEncodedName); + engine.FolderCache[encodedName] = oldFolder; + oldFolder.OnRenamed (encodedName, delim, attrs); + folder = oldFolder; + } } } - static string ReadNStringToken (ImapEngine engine, string format, bool rfc2047, CancellationToken cancellationToken) + static char ParseFolderSeparator (ImapToken token, string format) { - var token = engine.ReadToken (cancellationToken); - string value; + if (token.Type == ImapTokenType.QString) { + var qstring = (string) token.Value; - switch (token.Type) { - case ImapTokenType.Literal: - value = engine.ReadLiteral (cancellationToken); - break; - case ImapTokenType.QString: - case ImapTokenType.Atom: - value = (string) token.Value; - break; - case ImapTokenType.Nil: - return null; - default: + return qstring.Length > 0 ? qstring[0] : '\0'; + } else if (token.Type == ImapTokenType.Nil) { + return '\0'; + } else { throw ImapEngine.UnexpectedToken (format, token); } - - return rfc2047 ? Rfc2047.DecodeText (ImapEngine.Latin1.GetBytes (value)) : value; } - static uint ReadNumber (ImapEngine engine, string format, CancellationToken cancellationToken) + /// + /// Parses an untagged LIST or LSUB response. + /// + /// The IMAP engine. + /// The list of folders to be populated. + /// if it is an LSUB response; otherwise, . + /// if the LIST response is expected to return \Subscribed flags; otherwise, . + /// The cancellation token. + public static void ParseFolderList (ImapEngine engine, List? list, bool isLsub, bool returnsSubscribed, CancellationToken cancellationToken) { + var format = string.Format (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, isLsub ? "LSUB" : "LIST", "{0}"); var token = engine.ReadToken (cancellationToken); - uint number; + var attrs = FolderAttributes.None; + ImapFolder? folder = null; + string encodedName; + char delim; - if (token.Type != ImapTokenType.Atom || !uint.TryParse ((string) token.Value, out number)) - throw ImapEngine.UnexpectedToken (format, token); + // parse the folder attributes list + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, format, token); - return number; - } + token = engine.ReadToken (cancellationToken); - static bool NeedsQuoting (string value) - { - for (int i = 0; i < value.Length; i++) { - if (value[i] > 127 || char.IsControl (value[i])) - return true; + while (token.Type == ImapTokenType.Flag || token.Type == ImapTokenType.Atom) { + var atom = (string) token.Value; - if (QuotedSpecials.IndexOf (value[i]) != -1) - return true; + AddFolderAttribute (ref attrs, atom); + + token = engine.ReadToken (cancellationToken); } - return false; - } + ImapEngine.AssertToken (token, ImapTokenType.CloseParen, format, token); - static void ParseParameterList (StringBuilder builder, ImapEngine engine, string format, CancellationToken cancellationToken) - { - ImapToken token; + // parse the path delimiter + token = engine.ReadToken (cancellationToken); - do { - token = engine.PeekToken (cancellationToken); + delim = ParseFolderSeparator (token, format); - if (token.Type == ImapTokenType.CloseParen) - break; + encodedName = ReadFolderName (engine, format, true, cancellationToken); + encodedName = encodedName.TrimEnd (delim); - var name = ReadStringToken (engine, format, cancellationToken); + if (IsInbox (encodedName)) + attrs |= FolderAttributes.Inbox; - // Note: technically, the value should also be a 'string' token and not an 'nstring', - // but issue #124 reveals a server that is sending NIL for boundary values. - var value = ReadNStringToken (engine, format, false, cancellationToken) ?? string.Empty; + // peek at the next token to see if we have a LIST extension + token = engine.PeekToken (cancellationToken); - builder.Append ("; ").Append (name).Append ('='); + if (token.Type == ImapTokenType.OpenParen) { + // read the '(' token + engine.ReadToken (cancellationToken); - if (NeedsQuoting (value)) - builder.Append (MimeUtils.Quote (value)); - else - builder.Append (value); - } while (true); + do { + token = engine.ReadToken (cancellationToken); - // read the ')' - engine.ReadToken (cancellationToken); - } + if (token.Type == ImapTokenType.CloseParen) + break; - static bool ParseContentType (ImapEngine engine, string format, CancellationToken cancellationToken, out ContentType contentType, out string value) - { - var type = ReadNStringToken (engine, format, false, cancellationToken) ?? "application"; - var token = engine.PeekToken (cancellationToken); - string subtype; + // A LIST extension (rfc5258). - value = null; + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapTokenType.QString, format, token); - // Note: work around broken IMAP server implementations... - if (token.Type == ImapTokenType.OpenParen) { - if (engine.IsGMail) { - // Note: GMail's IMAP server implementation breaks when it encounters - // nested multiparts with the same boundary and returns a BODYSTRUCTURE - // like the example in https://github.com/jstedfast/MailKit/issues/205 - contentType = null; - value = type; - return false; - } + var property = (string) token.Value; - // Note: In other IMAP server implementations, such as the one found in - // https://github.com/jstedfast/MailKit/issues/371, if the server comes - // across something like "Content-Type: X-ZIP", it will only send a - // media-subtype token and completely fail to send a media-type token. - subtype = type; - type = "application"; - } else { - subtype = ReadNStringToken (engine, format, false, cancellationToken) ?? string.Empty; + token = engine.ReadToken (cancellationToken); + + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, format, token); + + do { + token = engine.ReadToken (cancellationToken); + + if (token.Type == ImapTokenType.CloseParen) + break; + + engine.UngetToken (token); + + var value = ReadNStringToken (engine, format, false, cancellationToken); + + ProcessListExtensionProperty (engine, ref folder, encodedName, delim, attrs, property, value); + } while (true); + } while (true); + } else { + ImapEngine.AssertToken (token, ImapTokenType.Eoln, format, token); + } + + AddFolder (engine, list, folder, encodedName, delim, attrs, isLsub, returnsSubscribed); + } + + + /// + /// Asynchronously parses an untagged LIST or LSUB response. + /// + /// The IMAP engine. + /// The list of folders to be populated. + /// if it is an LSUB response; otherwise, . + /// if the LIST response is expected to return \Subscribed flags; otherwise, . + /// The cancellation token. + public static async Task ParseFolderListAsync (ImapEngine engine, List? list, bool isLsub, bool returnsSubscribed, CancellationToken cancellationToken) + { + var format = string.Format (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, isLsub ? "LSUB" : "LIST", "{0}"); + var token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + var attrs = FolderAttributes.None; + ImapFolder? folder = null; + string encodedName; + char delim; + + // parse the folder attributes list + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, format, token); + + token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + while (token.Type == ImapTokenType.Flag || token.Type == ImapTokenType.Atom) { + var atom = (string) token.Value; + + AddFolderAttribute (ref attrs, atom); + + token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + } + + ImapEngine.AssertToken (token, ImapTokenType.CloseParen, format, token); + + // parse the path delimiter + token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + delim = ParseFolderSeparator (token, format); + + encodedName = await ReadFolderNameAsync (engine, format, true, cancellationToken).ConfigureAwait (false); + encodedName = encodedName.TrimEnd (delim); + + if (IsInbox (encodedName)) + attrs |= FolderAttributes.Inbox; + + // peek at the next token to see if we have a LIST extension + token = await engine.PeekTokenAsync (cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.OpenParen) { + // read the '(' token + await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + do { + token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.CloseParen) + break; + + // A LIST extension (rfc5258). + + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapTokenType.QString, format, token); + + var property = (string) token.Value; + + token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, format, token); + + do { + token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.CloseParen) + break; + + engine.UngetToken (token); + + var value = await ReadNStringTokenAsync (engine, format, false, cancellationToken).ConfigureAwait (false); + + ProcessListExtensionProperty (engine, ref folder, encodedName, delim, attrs, property, value); + } while (true); + } while (true); + } else { + ImapEngine.AssertToken (token, ImapTokenType.Eoln, format, token); + } + + AddFolder (engine, list, folder, encodedName, delim, attrs, isLsub, returnsSubscribed); + } + + /// + /// Handles an untagged LIST or LSUB response. + /// + /// The IMAP engine. + /// The IMAP command. + /// The index. + /// Whether or not asynchronous IO methods should be used. + public static Task UntaggedListHandler (ImapEngine engine, ImapCommand ic, int index, bool doAsync) + { + var list = (List) ic.UserData!; + + if (doAsync) + return ParseFolderListAsync (engine, list, ic.Lsub, ic.ListReturnsSubscribed, ic.CancellationToken); + + ParseFolderList (engine, list, ic.Lsub, ic.ListReturnsSubscribed, ic.CancellationToken); + + return Task.CompletedTask; + } + + /// + /// Parses an untagged METADATA response. + /// + /// The encoded name of the folder that the metadata belongs to. + /// The IMAP engine. + /// The metadata collection to be populated. + /// The cancellation token. + public static void ParseMetadata (ImapEngine engine, MetadataCollection metadata, CancellationToken cancellationToken) + { + var format = string.Format (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "METADATA", "{0}"); + var encodedName = ReadFolderName (engine, format, false, cancellationToken); + + var token = engine.ReadToken (cancellationToken); + + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, format, token); + + while (token.Type != ImapTokenType.CloseParen) { + var tag = ReadStringToken (engine, format, cancellationToken); + var value = ReadStringToken (engine, format, cancellationToken); + + metadata.Add (new Metadata (MetadataTag.Create (tag), value) { EncodedName = encodedName }); + + token = engine.PeekToken (cancellationToken); + } + + // read the closing paren + engine.ReadToken (cancellationToken); + } + + /// + /// Asynchronously parses an untagged METADATA response. + /// + /// The encoded name of the folder that the metadata belongs to. + /// The IMAP engine. + /// The metadata collection to be populated. + /// The cancellation token. + public static async Task ParseMetadataAsync (ImapEngine engine, MetadataCollection metadata, CancellationToken cancellationToken) + { + var format = string.Format (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "METADATA", "{0}"); + var encodedName = await ReadFolderNameAsync (engine, format, false, cancellationToken).ConfigureAwait (false); + + var token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, format, token); + + while (token.Type != ImapTokenType.CloseParen) { + var tag = await ReadStringTokenAsync (engine, format, cancellationToken).ConfigureAwait (false); + var value = await ReadStringTokenAsync (engine, format, cancellationToken).ConfigureAwait (false); + + metadata.Add (new Metadata (MetadataTag.Create (tag), value) { EncodedName = encodedName }); + + token = await engine.PeekTokenAsync (cancellationToken).ConfigureAwait (false); + } + + // read the closing paren + await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + } + + /// + /// Handles an untagged METADATA response. + /// + /// The IMAP engine. + /// The IMAP command. + /// The index. + /// Whether or not asynchronous IO methods should be used. + public static Task UntaggedMetadataHandler (ImapEngine engine, ImapCommand ic, int index, bool doAsync) + { + var metadata = (MetadataCollection) ic.UserData!; + + if (doAsync) + return ParseMetadataAsync (engine, metadata, ic.CancellationToken); + + ParseMetadata (engine, metadata, ic.CancellationToken); + + return Task.CompletedTask; + } + + internal static string ReadStringToken (ImapEngine engine, string format, CancellationToken cancellationToken) + { + var token = engine.ReadToken (cancellationToken); + + switch (token.Type) { + case ImapTokenType.Literal: + return engine.ReadLiteral (cancellationToken); + case ImapTokenType.QString: + case ImapTokenType.Atom: + return (string) token.Value; + default: + throw ImapEngine.UnexpectedToken (format, token); + } + } + + internal static async ValueTask ReadStringTokenAsync (ImapEngine engine, string format, CancellationToken cancellationToken) + { + var token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + switch (token.Type) { + case ImapTokenType.Literal: + return await engine.ReadLiteralAsync (cancellationToken).ConfigureAwait (false); + case ImapTokenType.QString: + case ImapTokenType.Atom: + return (string) token.Value; + default: + throw ImapEngine.UnexpectedToken (format, token); + } + } + + internal static string? ReadNStringToken (ImapEngine engine, string format, bool rfc2047, CancellationToken cancellationToken) + { + var token = engine.ReadToken (cancellationToken); + string value; + + switch (token.Type) { + case ImapTokenType.Literal: + value = engine.ReadLiteral (cancellationToken); + break; + case ImapTokenType.QString: + case ImapTokenType.Atom: + value = (string) token.Value; + break; + case ImapTokenType.Nil: + return null; + default: + throw ImapEngine.UnexpectedToken (format, token); + } + + if (rfc2047) + return Rfc2047.DecodeText (TextEncodings.UTF8.GetBytes (value)); + + return value; + } + + internal static async ValueTask ReadNStringTokenAsync (ImapEngine engine, string format, bool rfc2047, CancellationToken cancellationToken) + { + var token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + string value; + + switch (token.Type) { + case ImapTokenType.Literal: + value = await engine.ReadLiteralAsync (cancellationToken).ConfigureAwait (false); + break; + case ImapTokenType.QString: + case ImapTokenType.Atom: + value = (string) token.Value; + break; + case ImapTokenType.Nil: + return null; + default: + throw ImapEngine.UnexpectedToken (format, token); + } + + if (rfc2047) + return Rfc2047.DecodeText (TextEncodings.UTF8.GetBytes (value)); + + return value; + } + + static uint ParseNumberToken (ImapToken token, string format) + { + // Note: this is a work-around for broken IMAP servers that return negative integer values for things + // like octet counts and line counts. + if (token.Type == ImapTokenType.Atom) { + var atom = (string) token.Value; + + if (atom.Length > 0 && atom[0] == '-') { + if (!int.TryParse (atom, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out _)) + throw ImapEngine.UnexpectedToken (format, token); + + // Note: since Octets & Lines are the only 2 values this method is responsible for parsing, + // it seems the only sane value to return would be 0. + return 0; + } + } + + return ImapEngine.ParseNumber (token, false, format, token); + } + + static uint ReadNumber (ImapEngine engine, string format, CancellationToken cancellationToken) + { + var token = engine.ReadToken (cancellationToken); + + return ParseNumberToken (token, format); + } + + static async ValueTask ReadNumberAsync (ImapEngine engine, string format, CancellationToken cancellationToken) + { + var token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + return ParseNumberToken (token, format); + } + + static bool NeedsQuoting (string value) + { + for (int i = 0; i < value.Length; i++) { + if (value[i] > 127 || char.IsControl (value[i])) + return true; + + if (QuotedSpecials.IndexOf (value[i]) != -1) + return true; + } + + return value.Length == 0; + } + + static void ParseParameterList (StringBuilder builder, ImapEngine engine, string format, CancellationToken cancellationToken) + { + ImapToken token; + + do { + token = engine.PeekToken (cancellationToken); + + if (token.Type == ImapTokenType.CloseParen) + break; + + var name = ReadStringToken (engine, format, cancellationToken); + + // Note: technically, the value should also be a 'string' token and not an 'nstring', + // but issue #124 reveals a server that is sending NIL for boundary values. + var value = ReadNStringToken (engine, format, false, cancellationToken) ?? string.Empty; + + builder.Append ("; ").Append (name).Append ('='); + + if (NeedsQuoting (value)) + MimeUtils.AppendQuoted (builder, value); + else + builder.Append (value); + } while (true); + + // read the ')' + engine.ReadToken (cancellationToken); + } + + static async Task ParseParameterListAsync (StringBuilder builder, ImapEngine engine, string format, CancellationToken cancellationToken) + { + ImapToken token; + + do { + token = await engine.PeekTokenAsync (cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.CloseParen) + break; + + var name = await ReadStringTokenAsync (engine, format, cancellationToken).ConfigureAwait (false); + + // Note: technically, the value should also be a 'string' token and not an 'nstring', + // but issue #124 reveals a server that is sending NIL for boundary values. + var value = await ReadNStringTokenAsync (engine, format, false, cancellationToken).ConfigureAwait (false) ?? string.Empty; + + builder.Append ("; ").Append (name).Append ('='); + + if (NeedsQuoting (value)) + MimeUtils.AppendQuoted (builder, value); + else + builder.Append (value); + } while (true); + + // read the ')' + await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + } + + //static readonly string[] MediaTypes = new string[] { "text", "application", "audio", "image", "message", "multipart", "video" }; + + static bool IsMediaTypeWithDefaultSubtype (string type, [NotNullWhen (true)] out string? subtype) + { + if (type.Equals ("text", StringComparison.OrdinalIgnoreCase)) { + subtype = "plain"; + return true; + } + + if (type.Equals ("application", StringComparison.OrdinalIgnoreCase)) { + subtype = "octet-stream"; + return true; + } + + if (type.Equals ("multipart", StringComparison.OrdinalIgnoreCase)) { + subtype = "mixed"; + return true; + } + + // Note: if we ever decide to uncomment this, we'll *probably* have to modify ParseBodyPartAsync() unless + // we want it to construct a BodyPartMessage. Most likely this will depend on an actual test-case to know + // what the "correct" behavior should be. + //if (type.Equals ("message", StringComparison.OrdinalIgnoreCase)) { + // subtype = "rfc822"; + // return true; + //} + + subtype = null; + return false; + } + + static ContentType ParseContentType (ImapEngine engine, string format, CancellationToken cancellationToken) + { + var type = ReadNStringToken (engine, format, false, cancellationToken); + var token = engine.PeekToken (cancellationToken); + string? subtype; + + if (token.Type == ImapTokenType.OpenParen || token.Type == ImapTokenType.Nil) { + // Note: work around broken IMAP server implementations... + if (type == null) { + if (token.Type == ImapTokenType.Nil) { + // The type and subtype tokens are both NIL. We probably got something like: + // (NIL NIL NIL NIL NIL "7BIT" 0 NIL NIL NIL NIL) + // Consume the NIL subtype token. + engine.ReadToken (cancellationToken); + } + + type = "application"; + subtype = "octet-stream"; + } else { + // Note: In some IMAP server implementations, such as the one found in + // https://github.com/jstedfast/MailKit/issues/371, if the server comes + // across something like "Content-Type: X-ZIP", it will only send an + // empty string as the media-type. + // + // e.g. ( "X-ZIP" NIL ...) or ( "PLAIN" ("CHARSET" "US-ASCII") ...) + // + // Take special note of the leading character after the '('. + if (!IsMediaTypeWithDefaultSubtype (type, out subtype)) { + subtype = type; + type = "application"; + } + } + } else { + type ??= "application"; + subtype = ReadStringToken (engine, format, cancellationToken); } token = engine.ReadToken (cancellationToken); - if (token.Type == ImapTokenType.Nil) { + if (token.Type == ImapTokenType.Nil) + return new ContentType (type, subtype); + + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, format, token); + + var builder = new StringBuilder (); + builder.Append (type); + builder.Append ('/'); + builder.Append (subtype); + + ParseParameterList (builder, engine, format, cancellationToken); + + if (!ContentType.TryParse (builder.ToString (), out var contentType)) contentType = new ContentType (type, subtype); - return true; + + return contentType; + } + + static async Task ParseContentTypeAsync (ImapEngine engine, string format, CancellationToken cancellationToken) + { + var type = await ReadNStringTokenAsync (engine, format, false, cancellationToken).ConfigureAwait (false); + var token = await engine.PeekTokenAsync (cancellationToken).ConfigureAwait (false); + string? subtype; + + if (token.Type == ImapTokenType.OpenParen || token.Type == ImapTokenType.Nil) { + // Note: work around broken IMAP server implementations... + if (type == null) { + if (token.Type == ImapTokenType.Nil) { + // The type and subtype tokens are both NIL. We probably got something like: + // (NIL NIL NIL NIL NIL "7BIT" 0 NIL NIL NIL NIL) + // Consume the NIL subtype token. + await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + } + + type = "application"; + subtype = "octet-stream"; + } else { + // Note: In some IMAP server implementations, such as the one found in + // https://github.com/jstedfast/MailKit/issues/371, if the server comes + // across something like "Content-Type: X-ZIP", it will only send an + // empty string as the media-type. + // + // e.g. ( "X-ZIP" NIL ...) or ( "PLAIN" ("CHARSET" "US-ASCII") ...) + // + // Take special note of the leading character after the '('. + if (!IsMediaTypeWithDefaultSubtype (type, out subtype)) { + subtype = type; + type = "application"; + } + } + } else { + type ??= "application"; + subtype = await ReadStringTokenAsync (engine, format, cancellationToken).ConfigureAwait (false); } - if (token.Type != ImapTokenType.OpenParen) + token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.Nil) + return new ContentType (type, subtype); + + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, format, token); + + var builder = new StringBuilder (); + builder.Append (type); + builder.Append ('/'); + builder.Append (subtype); + + await ParseParameterListAsync (builder, engine, format, cancellationToken).ConfigureAwait (false); + + if (!ContentType.TryParse (builder.ToString (), out var contentType)) + contentType = new ContentType (type, subtype); + + return contentType; + } + + static ContentDisposition? ParseContentDisposition (ImapEngine engine, string format, CancellationToken cancellationToken) + { + // body-fld-dsp = "(" string SP body-fld-param ")" / nil + var token = engine.ReadToken (cancellationToken); + + if (token.Type == ImapTokenType.Nil) + return null; + + if (token.Type != ImapTokenType.OpenParen) { + // Note: this is a work-around for issue #919 where Exchange sends `"inline"` instead of `("inline" NIL)` + if (token.Type == ImapTokenType.Atom || token.Type == ImapTokenType.QString) + return new ContentDisposition ((string) token.Value); + throw ImapEngine.UnexpectedToken (format, token); + } + + // Exchange bug: ... (NIL NIL) ... + var dsp = ReadNStringToken (engine, format, false, cancellationToken); + var builder = new StringBuilder (); + bool isNil = false; + + // Note: These are work-arounds for some bugs in some mail clients that + // either leave out the disposition value or quote it. + // + // See https://github.com/jstedfast/MailKit/issues/486 for details. + if (string.IsNullOrEmpty (dsp)) + builder.Append (ContentDisposition.Attachment); + else + builder.Append (dsp!.Trim ('"')); + + token = engine.ReadToken (cancellationToken); + + if (token.Type == ImapTokenType.OpenParen) + ParseParameterList (builder, engine, format, cancellationToken); + else if (token.Type != ImapTokenType.Nil) + throw ImapEngine.UnexpectedToken (format, token); + else + isNil = true; + + token = engine.ReadToken (cancellationToken); - var builder = new StringBuilder (); - builder.AppendFormat ("{0}/{1}", type, subtype); + ImapEngine.AssertToken (token, ImapTokenType.CloseParen, format, token); - ParseParameterList (builder, engine, format, cancellationToken); + if (dsp == null && isNil) + return null; - if (!ContentType.TryParse (builder.ToString (), out contentType)) - contentType = new ContentType (type, subtype); + ContentDisposition.TryParse (builder.ToString (), out var disposition); - return true; + return disposition; } - static ContentDisposition ParseContentDisposition (ImapEngine engine, string format, CancellationToken cancellationToken) + static async Task ParseContentDispositionAsync (ImapEngine engine, string format, CancellationToken cancellationToken) { - var token = engine.ReadToken (cancellationToken); + // body-fld-dsp = "(" string SP body-fld-param ")" / nil + var token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); if (token.Type == ImapTokenType.Nil) return null; - if (token.Type != ImapTokenType.OpenParen) + if (token.Type != ImapTokenType.OpenParen) { + // Note: this is a work-around for issue #919 where Exchange sends `"inline"` instead of `("inline" NIL)` + if (token.Type == ImapTokenType.Atom || token.Type == ImapTokenType.QString) + return new ContentDisposition ((string) token.Value); + throw ImapEngine.UnexpectedToken (format, token); + } - var dsp = ReadStringToken (engine, format, cancellationToken); + // Exchange bug: ... (NIL NIL) ... + var dsp = await ReadNStringTokenAsync (engine, format, false, cancellationToken).ConfigureAwait (false); + var builder = new StringBuilder (); + bool isNil = false; // Note: These are work-arounds for some bugs in some mail clients that // either leave out the disposition value or quote it. // // See https://github.com/jstedfast/MailKit/issues/486 for details. if (string.IsNullOrEmpty (dsp)) - dsp = ContentDisposition.Attachment; + builder.Append (ContentDisposition.Attachment); else - dsp = dsp.Trim ('"'); + builder.Append (dsp!.Trim ('"')); - var builder = new StringBuilder (dsp); - ContentDisposition disposition; - - token = engine.ReadToken (cancellationToken); + token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); if (token.Type == ImapTokenType.OpenParen) - ParseParameterList (builder, engine, format, cancellationToken); + await ParseParameterListAsync (builder, engine, format, cancellationToken).ConfigureAwait (false); else if (token.Type != ImapTokenType.Nil) throw ImapEngine.UnexpectedToken (format, token); + else + isNil = true; - token = engine.ReadToken (cancellationToken); + token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); - if (token.Type != ImapTokenType.CloseParen) - throw ImapEngine.UnexpectedToken (format, token); + ImapEngine.AssertToken (token, ImapTokenType.CloseParen, format, token); + + if (dsp == null && isNil) + return null; - ContentDisposition.TryParse (builder.ToString (), out disposition); + ContentDisposition.TryParse (builder.ToString (), out var disposition); return disposition; } - static string[] ParseContentLanguage (ImapEngine engine, string format, CancellationToken cancellationToken) + static string[]? ParseContentLanguage (ImapEngine engine, string format, CancellationToken cancellationToken) { var token = engine.ReadToken (cancellationToken); var languages = new List (); - string language; + string? language; switch (token.Type) { case ImapTokenType.Literal: @@ -704,8 +1385,13 @@ static string[] ParseContentLanguage (ImapEngine engine, string format, Cancella if (token.Type == ImapTokenType.CloseParen) break; - language = ReadStringToken (engine, format, cancellationToken); - languages.Add (language); + // Note: Some broken IMAP servers send `NIL` tokens in this list. Just ignore them. + // + // See https://github.com/jstedfast/MailKit/issues/953 + language = ReadNStringToken (engine, format, false, cancellationToken); + + if (language != null) + languages.Add (language); } while (true); // read the ')' @@ -718,155 +1404,640 @@ static string[] ParseContentLanguage (ImapEngine engine, string format, Cancella return languages.ToArray (); } - static Uri ParseContentLocation (ImapEngine engine, string format, CancellationToken cancellationToken) + static async Task ParseContentLanguageAsync (ImapEngine engine, string format, CancellationToken cancellationToken) { - var location = ReadNStringToken (engine, format, false, cancellationToken); + var token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + var languages = new List (); + string? language; + + switch (token.Type) { + case ImapTokenType.Literal: + language = await engine.ReadLiteralAsync (cancellationToken).ConfigureAwait (false); + languages.Add (language); + break; + case ImapTokenType.QString: + case ImapTokenType.Atom: + language = (string) token.Value; + languages.Add (language); + break; + case ImapTokenType.Nil: + return null; + case ImapTokenType.OpenParen: + do { + token = await engine.PeekTokenAsync (cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.CloseParen) + break; + + // Note: Some broken IMAP servers send `NIL` tokens in this list. Just ignore them. + // + // See https://github.com/jstedfast/MailKit/issues/953 + language = await ReadNStringTokenAsync (engine, format, false, cancellationToken).ConfigureAwait (false); + + if (language != null) + languages.Add (language); + } while (true); + + // read the ')' + await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + break; + default: + throw ImapEngine.UnexpectedToken (format, token); + } + + return languages.ToArray (); + } + static Uri? ParseContentLocation (string? location) + { if (string.IsNullOrWhiteSpace (location)) return null; - if (Uri.IsWellFormedUriString (location, UriKind.Absolute)) - return new Uri (location, UriKind.Absolute); + if (Uri.IsWellFormedUriString (location, UriKind.Absolute)) + return new Uri (location, UriKind.Absolute); + + if (Uri.IsWellFormedUriString (location, UriKind.Relative)) + return new Uri (location, UriKind.Relative); + + return null; + } + + static Uri? ParseContentLocation (ImapEngine engine, string format, CancellationToken cancellationToken) + { + var location = ReadNStringToken (engine, format, false, cancellationToken); + + return ParseContentLocation (location); + } + + static async Task ParseContentLocationAsync (ImapEngine engine, string format, CancellationToken cancellationToken) + { + var location = await ReadNStringTokenAsync (engine, format, false, cancellationToken).ConfigureAwait (false); + + return ParseContentLocation (location); + } + + static void SkipBodyExtension (ImapEngine engine, string format, CancellationToken cancellationToken) + { + var token = engine.ReadToken (cancellationToken); + + switch (token.Type) { + case ImapTokenType.OpenParen: + do { + token = engine.PeekToken (cancellationToken); + + if (token.Type == ImapTokenType.CloseParen) + break; + + SkipBodyExtension (engine, format, cancellationToken); + } while (true); + + // read the ')' + engine.ReadToken (cancellationToken); + break; + case ImapTokenType.Literal: + engine.ReadLiteral (cancellationToken); + break; + case ImapTokenType.QString: + case ImapTokenType.Atom: + case ImapTokenType.Nil: + break; + default: + throw ImapEngine.UnexpectedToken (format, token); + } + } + + static async Task SkipBodyExtensionAsync (ImapEngine engine, string format, CancellationToken cancellationToken) + { + var token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + switch (token.Type) { + case ImapTokenType.OpenParen: + do { + token = await engine.PeekTokenAsync (cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.CloseParen) + break; + + await SkipBodyExtensionAsync (engine, format, cancellationToken).ConfigureAwait (false); + } while (true); + + // read the ')' + await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + break; + case ImapTokenType.Literal: + await engine.ReadLiteralAsync (cancellationToken).ConfigureAwait (false); + break; + case ImapTokenType.QString: + case ImapTokenType.Atom: + case ImapTokenType.Nil: + break; + default: + throw ImapEngine.UnexpectedToken (format, token); + } + } + + static BodyPart ParseMultipart (ImapEngine engine, string format, string path, CancellationToken cancellationToken) + { + var prefix = path.Length > 0 ? path + "." : string.Empty; + var bodyParts = new BodyPartCollection (); + ImapToken token; + int index = 1; + + token = engine.PeekToken (cancellationToken); + + if (token.Type != ImapTokenType.Nil) { + do { + var part = ParseBody (engine, format, prefix + index, cancellationToken); + + if (part != null) + bodyParts.Add (part); + + token = engine.PeekToken (cancellationToken); + index++; + } while (token.Type == ImapTokenType.OpenParen); + } else { + // Note: Sometimes, when a multipart contains no children, IMAP servers (even Dovecot!) + // will reply with a BODYSTRUCTURE that looks like (NIL "alternative" ("boundary" "... + // Obviously, this is not a body-type-1part because "alternative" is a multipart subtype. + // This suggests that the NIL represents an empty list of children. + // + // See https://github.com/jstedfast/MailKit/issues/1393 for more details. + engine.ReadToken (cancellationToken); + } + + var subtype = ReadStringToken (engine, format, cancellationToken); + var contentType = new ContentType ("multipart", subtype); + var body = new BodyPartMultipart (contentType, path, bodyParts); + + token = engine.PeekToken (cancellationToken); + + if (token.Type != ImapTokenType.CloseParen) { + token = engine.ReadToken (cancellationToken); + + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, ImapTokenType.Nil, format, token); + + var builder = new StringBuilder (); + builder.Append (body.ContentType.MediaType); + builder.Append ('/'); + builder.Append (body.ContentType.MediaSubtype); + + if (token.Type == ImapTokenType.OpenParen) + ParseParameterList (builder, engine, format, cancellationToken); + + if (ContentType.TryParse (builder.ToString (), out contentType)) + body.ContentType = contentType; + + token = engine.PeekToken (cancellationToken); + } + + if (token.Type == ImapTokenType.QString) { + // Note: This is a work-around for broken Exchange servers. + // + // See https://stackoverflow.com/questions/33481604/mailkit-fetch-unexpected-token-in-imap-response-qstring-multipart-message + // for details. + + // Read what appears to be a Content-Description. + token = engine.ReadToken (cancellationToken); + + // Peek ahead at the next token. It has been suggested that this next token seems to be the Content-Language value. + token = engine.PeekToken (cancellationToken); + } else if (token.Type != ImapTokenType.CloseParen) { + body.ContentDisposition = ParseContentDisposition (engine, format, cancellationToken); + token = engine.PeekToken (cancellationToken); + } + + if (token.Type != ImapTokenType.CloseParen) { + body.ContentLanguage = ParseContentLanguage (engine, format, cancellationToken); + token = engine.PeekToken (cancellationToken); + } + + if (token.Type != ImapTokenType.CloseParen) { + body.ContentLocation = ParseContentLocation (engine, format, cancellationToken); + token = engine.PeekToken (cancellationToken); + } + + while (token.Type != ImapTokenType.CloseParen) { + SkipBodyExtension (engine, format, cancellationToken); + token = engine.PeekToken (cancellationToken); + } + + // read the ')' + engine.ReadToken (cancellationToken); + + return body; + } + + static async Task ParseMultipartAsync (ImapEngine engine, string format, string path, CancellationToken cancellationToken) + { + var prefix = path.Length > 0 ? path + "." : string.Empty; + var bodyParts = new BodyPartCollection (); + ImapToken token; + int index = 1; + + token = await engine.PeekTokenAsync (cancellationToken).ConfigureAwait (false); + + if (token.Type != ImapTokenType.Nil) { + do { + var part = await ParseBodyAsync (engine, format, prefix + index, cancellationToken).ConfigureAwait (false); + + if (part != null) + bodyParts.Add (part); + + token = await engine.PeekTokenAsync (cancellationToken).ConfigureAwait (false); + index++; + } while (token.Type == ImapTokenType.OpenParen); + } else { + // Note: Sometimes, when a multipart contains no children, IMAP servers (even Dovecot!) + // will reply with a BODYSTRUCTURE that looks like (NIL "alternative" ("boundary" "... + // Obviously, this is not a body-type-1part because "alternative" is a multipart subtype. + // This suggests that the NIL represents an empty list of children. + // + // See https://github.com/jstedfast/MailKit/issues/1393 for more details. + await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + } + + var subtype = await ReadStringTokenAsync (engine, format, cancellationToken).ConfigureAwait (false); + var contentType = new ContentType ("multipart", subtype); + var body = new BodyPartMultipart (contentType, path, bodyParts); + + token = await engine.PeekTokenAsync (cancellationToken).ConfigureAwait (false); + + if (token.Type != ImapTokenType.CloseParen) { + token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, ImapTokenType.Nil, format, token); + + var builder = new StringBuilder (); + builder.Append (body.ContentType.MediaType); + builder.Append ('/'); + builder.Append (body.ContentType.MediaSubtype); + + if (token.Type == ImapTokenType.OpenParen) + await ParseParameterListAsync (builder, engine, format, cancellationToken).ConfigureAwait (false); + + if (ContentType.TryParse (builder.ToString (), out contentType)) + body.ContentType = contentType; + + token = await engine.PeekTokenAsync (cancellationToken).ConfigureAwait (false); + } + + if (token.Type == ImapTokenType.QString) { + // Note: This is a work-around for broken Exchange servers. + // + // See https://stackoverflow.com/questions/33481604/mailkit-fetch-unexpected-token-in-imap-response-qstring-multipart-message + // for details. + + // Read what appears to be a Content-Description. + token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + // Peek ahead at the next token. It has been suggested that this next token seems to be the Content-Language value. + token = await engine.PeekTokenAsync (cancellationToken).ConfigureAwait (false); + } else if (token.Type != ImapTokenType.CloseParen) { + body.ContentDisposition = await ParseContentDispositionAsync (engine, format, cancellationToken).ConfigureAwait (false); + token = await engine.PeekTokenAsync (cancellationToken).ConfigureAwait (false); + } + + if (token.Type != ImapTokenType.CloseParen) { + body.ContentLanguage = await ParseContentLanguageAsync (engine, format, cancellationToken).ConfigureAwait (false); + token = await engine.PeekTokenAsync (cancellationToken).ConfigureAwait (false); + } + + if (token.Type != ImapTokenType.CloseParen) { + body.ContentLocation = await ParseContentLocationAsync (engine, format, cancellationToken).ConfigureAwait (false); + token = await engine.PeekTokenAsync (cancellationToken).ConfigureAwait (false); + } + + while (token.Type != ImapTokenType.CloseParen) { + await SkipBodyExtensionAsync (engine, format, cancellationToken).ConfigureAwait (false); + token = await engine.PeekTokenAsync (cancellationToken).ConfigureAwait (false); + } + + // read the ')' + await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + return body; + } + + static bool ShouldParseMultipart (ImapEngine engine, CancellationToken cancellationToken) + { + var token = engine.ReadToken (cancellationToken); + ImapToken nextToken; + + switch (token.Type) { + case ImapTokenType.Atom: // Note: Technically, we should never get an Atom here, but if we do, we'll treat it as a QString. + case ImapTokenType.QString: + case ImapTokenType.Literal: + if (engine.QuirksMode is ImapQuirksMode.GMail or ImapQuirksMode.QQMail or ImapQuirksMode.Yandex && token.Type != ImapTokenType.Literal) { + // Note: GMail's IMAP server implementation breaks when it encounters nested multiparts with the same + // boundary and returns a BODYSTRUCTURE like the example in https://github.com/jstedfast/MailKit/issues/205 + // or like the example in https://github.com/jstedfast/MailKit/issues/777. There's also an issue with BODY + // responses like the one in https://github.com/jstedfast/MailKit/issues/1841. + // + // ("ALTERNATIVE" ("BOUNDARY" "==alternative_xad5934455aeex") NIL NIL) + // or + // ("RELATED" NIL ("ATTACHMENT" NIL) NIL) + // or + // ("ALTERNATIVE") + // + // Check if the next token is either a '(', ')' or NIL. + // + // If it is '(', then that would indicate the start of the Content-Type parameter list. + // If it is ')', then that would indicate a BODY response without a Content-Type parameter list. + // If it is NIL, then it would signify that the Content-Type has no parameters. + // + // Note: Yandex also has this problem. See https://github.com/jstedfast/MailKit/issues/1861 + // As does QQMail: https://github.com/jstedfast/MailKit/issues/1076 + + // Peek at the next token to see what we've got. If we get a '(' or NIL, then treat this as a multipart. + nextToken = engine.PeekToken (cancellationToken); + + if (nextToken.Type == ImapTokenType.OpenParen || nextToken.Type == ImapTokenType.CloseParen || nextToken.Type == ImapTokenType.Nil) { + // Unget the multipart subtype. + engine.UngetToken (token); + + // Now unget a fake NIL token that represents an empty set of children. + engine.UngetToken (ImapToken.Nil); + + return true; + } + + // Fall through and treat things normally. + } + + // We've got a string which normally means it's the first token of a mime-type. + engine.UngetToken (token); + return false; + case ImapTokenType.OpenParen: + // We've got children, so this is definitely a multipart. + engine.UngetToken (token); + return true; + case ImapTokenType.Nil: + // We've got a NIL token. Technically, this is illegal syntax, but we need to be able to handle it. + // + // There are currently 2 known examples of this: + // + // 1. Sometimes, when a multipart contains no children, IMAP servers (even Dovecot!) + // will reply with a BODYSTRUCTURE that looks like (NIL "alternative" ("boundary" "... + // Obviously, this is not a body-type-1part because "alternative" is a multipart subtype. + // This suggests that the NIL represents an empty list of children. + // + // For an example of this particular case, see https://github.com/jstedfast/MailKit/issues/1393. + // + // 2. There have been several reports of Office365 sending body-type-1parts of the following form: + // (NIL NIL NIL NIL NIL "7BIT" 0 NIL NIL NIL NIL) + // + // Presumably this is a text/plain part with no headers? + // + // For examples of this, see: + // https://github.com/jstedfast/MailKit/issues/1415#issuecomment-1206533214 and + // https://github.com/jstedfast/MailKit/issues/1446 + nextToken = engine.PeekToken (cancellationToken); + + engine.UngetToken (token); - if (Uri.IsWellFormedUriString (location, UriKind.Relative)) - return new Uri (location, UriKind.Relative); + if (nextToken.Type == ImapTokenType.Nil) { + // Looks like we've probably encountered the `(NIL NIL NIL NIL NIL "7BIT" 0 NIL NIL NIL NIL)` variant. + return false; + } - return null; + // Assume (NIL "alternative" ("boundary" "... + return true; + default: + engine.UngetToken (token); + return false; + } } - static void SkipBodyExtensions (ImapEngine engine, string format, CancellationToken cancellationToken) + static async Task ShouldParseMultipartAsync (ImapEngine engine, CancellationToken cancellationToken) { - var token = engine.ReadToken (cancellationToken); + var token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + ImapToken nextToken; switch (token.Type) { + case ImapTokenType.Atom: // Note: Technically, we should never get an Atom here, but if we do, we'll treat it as a QString. + case ImapTokenType.QString: + case ImapTokenType.Literal: + if (engine.QuirksMode is ImapQuirksMode.GMail or ImapQuirksMode.QQMail or ImapQuirksMode.Yandex && token.Type != ImapTokenType.Literal) { + // Note: GMail's IMAP server implementation breaks when it encounters nested multiparts with the same + // boundary and returns a BODYSTRUCTURE like the example in https://github.com/jstedfast/MailKit/issues/205 + // or like the example in https://github.com/jstedfast/MailKit/issues/777. There's also an issue with BODY + // responses like the one in https://github.com/jstedfast/MailKit/issues/1841. + // + // ("ALTERNATIVE" ("BOUNDARY" "==alternative_xad5934455aeex") NIL NIL) + // or + // ("RELATED" NIL ("ATTACHMENT" NIL) NIL) + // or + // ("ALTERNATIVE") + // + // Check if the next token is either a '(', ')' or NIL. + // + // If it is '(', then that would indicate the start of the Content-Type parameter list. + // If it is ')', then that would indicate a BODY response without a Content-Type parameter list. + // If it is NIL, then it would signify that the Content-Type has no parameters. + // + // Note: Yandex also has this problem. See https://github.com/jstedfast/MailKit/issues/1861 + // As does QQMail: https://github.com/jstedfast/MailKit/issues/1076 + + // Peek at the next token to see what we've got. If we get a '(' or NIL, then treat this as a multipart. + nextToken = await engine.PeekTokenAsync (cancellationToken).ConfigureAwait (false); + + if (nextToken.Type == ImapTokenType.OpenParen || nextToken.Type == ImapTokenType.CloseParen || nextToken.Type == ImapTokenType.Nil) { + // Unget the multipart subtype. + engine.UngetToken (token); + + // Now unget a fake NIL token that represents an empty set of children. + engine.UngetToken (ImapToken.Nil); + + return true; + } + + // Fall through and treat things nomrally. + } + + // We've got a string which normally means it's the first token of a mime-type. + engine.UngetToken (token); + return false; case ImapTokenType.OpenParen: - do { - token = engine.PeekToken (cancellationToken); + // We've got children, so this is definitely a multipart. + engine.UngetToken (token); + return true; + case ImapTokenType.Nil: + // We've got a NIL token. Technically, this is illegal syntax, but we need to be able to handle it. + // + // There are currently 2 known examples of this: + // + // 1. Sometimes, when a multipart contains no children, IMAP servers (even Dovecot!) + // will reply with a BODYSTRUCTURE that looks like (NIL "alternative" ("boundary" "... + // Obviously, this is not a body-type-1part because "alternative" is a multipart subtype. + // This suggests that the NIL represents an empty list of children. + // + // For an example of this particular case, see https://github.com/jstedfast/MailKit/issues/1393. + // + // 2. There have been several reports of Office365 sending body-type-1parts of the following form: + // (NIL NIL NIL NIL NIL "7BIT" 0 NIL NIL NIL NIL) + // + // Presumably this is a text/plain part with no headers? + // + // For examples of this, see: + // https://github.com/jstedfast/MailKit/issues/1415#issuecomment-1206533214 and + // https://github.com/jstedfast/MailKit/issues/1446 + nextToken = await engine.PeekTokenAsync (cancellationToken).ConfigureAwait (false); - if (token.Type == ImapTokenType.CloseParen) - break; + engine.UngetToken (token); - SkipBodyExtensions (engine, format, cancellationToken); - } while (true); + if (nextToken.Type == ImapTokenType.Nil) { + // Looks like we've probably encountered the `(NIL NIL NIL NIL NIL "7BIT" 0 NIL NIL NIL NIL)` variant. + return false; + } - // read the ')' - engine.ReadToken (cancellationToken); - break; - case ImapTokenType.Literal: - engine.ReadLiteral (cancellationToken); - break; - case ImapTokenType.QString: - case ImapTokenType.Atom: - case ImapTokenType.Nil: - break; + // Assume (NIL "alternative" ("boundary" "... + return true; default: - throw ImapEngine.UnexpectedToken (format, token); + engine.UngetToken (token); + return false; } } - static BodyPart ParseMultipart (ImapEngine engine, string format, string path, string subtype, CancellationToken cancellationToken) + public static BodyPart? ParseBody (ImapEngine engine, string format, string path, CancellationToken cancellationToken) { - var prefix = path.Length > 0 ? path + "." : string.Empty; - var body = new BodyPartMultipart (); - ImapToken token; - int index = 1; - - // Note: if subtype is not null, then we are working around a GMail bug... - if (subtype == null) { - do { - body.BodyParts.Add (ParseBody (engine, format, prefix + index, cancellationToken)); - token = engine.PeekToken (cancellationToken); - index++; - } while (token.Type == ImapTokenType.OpenParen); + var token = engine.ReadToken (cancellationToken); - subtype = ReadStringToken (engine, format, cancellationToken); - } + if (token.Type == ImapTokenType.Nil) + return null; - body.ContentType = new ContentType ("multipart", subtype); - body.PartSpecifier = path; + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, format, token); token = engine.PeekToken (cancellationToken); - if (token.Type != ImapTokenType.CloseParen) { - token = engine.ReadToken (cancellationToken); + // Note: If we immediately get a closing ')', then treat it the same as if we had gotten a `NIL` `body` token. + // + // See https://github.com/jstedfast/MailKit/issues/944 for details. + if (token.Type == ImapTokenType.CloseParen) { + engine.ReadToken (cancellationToken); + return null; + } - if (token.Type != ImapTokenType.OpenParen && token.Type != ImapTokenType.Nil) - throw ImapEngine.UnexpectedToken (format, token); + if (ShouldParseMultipart (engine, cancellationToken)) + return ParseMultipart (engine, format, path, cancellationToken); - var builder = new StringBuilder (); - ContentType contentType; + var type = ParseContentType (engine, format, cancellationToken); + var id = ReadNStringToken (engine, format, false, cancellationToken); + var desc = ReadNStringToken (engine, format, true, cancellationToken); + // Note: technically, body-fld-enc, is not allowed to be NIL, but we need to deal with broken servers... + var enc = ReadNStringToken (engine, format, false, cancellationToken); + var octets = ReadNumber (engine, format, cancellationToken); + var isMultipart = false; + BodyPartBasic body; - builder.AppendFormat ("{0}/{1}", body.ContentType.MediaType, body.ContentType.MediaSubtype); + if (type.IsMimeType ("message", "rfc822")) { + var rfc822 = new BodyPartMessage (type, path); - if (token.Type == ImapTokenType.OpenParen) - ParseParameterList (builder, engine, format, cancellationToken); + // Note: GMail (and potentially other IMAP servers) will send body-part-basic + // expressions instead of body-part-msg expressions when they encounter + // message/rfc822 MIME parts that are illegally encoded using base64 (or + // quoted-printable?). According to rfc3501, IMAP servers are REQUIRED to + // send body-part-msg expressions for message/rfc822 parts, however, it is + // understandable why GMail (and other IMAP servers?) do what they do in this + // particular case. + // + // For examples, see issue #32 and issue #59. + // + // The workaround is to check for the expected '(' signifying an envelope token. + // If we do not get an '(', then we are likely looking at the Content-MD5 token + // which gets handled below. + token = engine.PeekToken (cancellationToken); - if (ContentType.TryParse (builder.ToString (), out contentType)) - body.ContentType = contentType; + if (token.Type == ImapTokenType.OpenParen) { + rfc822.Envelope = ParseEnvelope (engine, cancellationToken); + rfc822.Body = ParseBody (engine, format, path, cancellationToken); + rfc822.Lines = ReadNumber (engine, format, cancellationToken); + } - token = engine.PeekToken (cancellationToken); + body = rfc822; + } else if (type.IsMimeType ("text", "*")) { + var text = new BodyPartText (type, path) { + Lines = ReadNumber (engine, format, cancellationToken) + }; + body = text; + } else { + isMultipart = type.IsMimeType ("multipart", "*"); + body = new BodyPartBasic (type, path); } - if (token.Type != ImapTokenType.CloseParen) { - body.ContentDisposition = ParseContentDisposition (engine, format, cancellationToken); - token = engine.PeekToken (cancellationToken); - } + body.ContentTransferEncoding = enc; + body.ContentDescription = desc; + body.ContentId = id; + body.Octets = octets; - if (token.Type != ImapTokenType.CloseParen) { - body.ContentLanguage = ParseContentLanguage (engine, format, cancellationToken); - token = engine.PeekToken (cancellationToken); + // if we are parsing a BODYSTRUCTURE, we may get some more tokens before the ')' + token = engine.PeekToken (cancellationToken); + + if (!isMultipart) { + if (token.Type != ImapTokenType.CloseParen) { + body.ContentMd5 = ReadNStringToken (engine, format, false, cancellationToken); + token = engine.PeekToken (cancellationToken); + } + + if (token.Type != ImapTokenType.CloseParen) { + body.ContentDisposition = ParseContentDisposition (engine, format, cancellationToken); + token = engine.PeekToken (cancellationToken); + } + + if (token.Type != ImapTokenType.CloseParen) { + body.ContentLanguage = ParseContentLanguage (engine, format, cancellationToken); + token = engine.PeekToken (cancellationToken); + } + + if (token.Type != ImapTokenType.CloseParen && token.Type != ImapTokenType.OpenParen) { + body.ContentLocation = ParseContentLocation (engine, format, cancellationToken); + token = engine.PeekToken (cancellationToken); + } } - if (token.Type != ImapTokenType.CloseParen) { - body.ContentLocation = ParseContentLocation (engine, format, cancellationToken); + while (token.Type != ImapTokenType.CloseParen) { + SkipBodyExtension (engine, format, cancellationToken); token = engine.PeekToken (cancellationToken); } - if (token.Type != ImapTokenType.CloseParen) - SkipBodyExtensions (engine, format, cancellationToken); - // read the ')' - token = engine.ReadToken (cancellationToken); - - if (token.Type != ImapTokenType.CloseParen) - throw ImapEngine.UnexpectedToken (format, token); + engine.ReadToken (cancellationToken); return body; } - public static BodyPart ParseBody (ImapEngine engine, string format, string path, CancellationToken cancellationToken) + public static async Task ParseBodyAsync (ImapEngine engine, string format, string path, CancellationToken cancellationToken) { - var token = engine.ReadToken (cancellationToken); + var token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); if (token.Type == ImapTokenType.Nil) return null; - if (token.Type != ImapTokenType.OpenParen) - throw ImapEngine.UnexpectedToken (format, token); - - token = engine.PeekToken (cancellationToken); - - if (token.Type == ImapTokenType.OpenParen) - return ParseMultipart (engine, format, path, null, cancellationToken); + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, format, token); - ContentType type; - string value; + token = await engine.PeekTokenAsync (cancellationToken).ConfigureAwait (false); - if (!ParseContentType (engine, format, cancellationToken, out type, out value)) { - // GMail breakage... yay! What we have is a nested multipart with - // the same boundary as its parent. - return ParseMultipart (engine, format, path, value, cancellationToken); + // Note: If we immediately get a closing ')', then treat it the same as if we had gotten a `NIL` `body` token. + // + // See https://github.com/jstedfast/MailKit/issues/944 for details. + if (token.Type == ImapTokenType.CloseParen) { + await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + return null; } - var id = ReadNStringToken (engine, format, false, cancellationToken); - var desc = ReadNStringToken (engine, format, true, cancellationToken); + if (await ShouldParseMultipartAsync (engine, cancellationToken).ConfigureAwait (false)) + return await ParseMultipartAsync (engine, format, path, cancellationToken).ConfigureAwait (false); + + var type = await ParseContentTypeAsync (engine, format, cancellationToken).ConfigureAwait (false); + var id = await ReadNStringTokenAsync (engine, format, false, cancellationToken).ConfigureAwait (false); + var desc = await ReadNStringTokenAsync (engine, format, true, cancellationToken).ConfigureAwait (false); // Note: technically, body-fld-enc, is not allowed to be NIL, but we need to deal with broken servers... - var enc = ReadNStringToken (engine, format, false, cancellationToken); - var octets = ReadNumber (engine, format, cancellationToken); + var enc = await ReadNStringTokenAsync (engine, format, false, cancellationToken).ConfigureAwait (false); + var octets = await ReadNumberAsync (engine, format, cancellationToken).ConfigureAwait (false); + var isMultipart = false; BodyPartBasic body; if (type.IsMimeType ("message", "rfc822")) { - var mesg = new BodyPartMessage (); + var rfc822 = new BodyPartMessage (type, path); // Note: GMail (and potentially other IMAP servers) will send body-part-basic // expressions instead of body-part-msg expressions when they encounter @@ -881,73 +2052,74 @@ public static BodyPart ParseBody (ImapEngine engine, string format, string path, // The workaround is to check for the expected '(' signifying an envelope token. // If we do not get an '(', then we are likely looking at the Content-MD5 token // which gets handled below. - token = engine.PeekToken (cancellationToken); + token = await engine.PeekTokenAsync (cancellationToken).ConfigureAwait (false); if (token.Type == ImapTokenType.OpenParen) { - mesg.Envelope = ParseEnvelope (engine, cancellationToken); - mesg.Body = ParseBody (engine, format, path, cancellationToken); - mesg.Lines = ReadNumber (engine, format, cancellationToken); + rfc822.Envelope = await ParseEnvelopeAsync (engine, cancellationToken).ConfigureAwait (false); + rfc822.Body = await ParseBodyAsync (engine, format, path, cancellationToken).ConfigureAwait (false); + rfc822.Lines = await ReadNumberAsync (engine, format, cancellationToken).ConfigureAwait (false); } - body = mesg; + body = rfc822; } else if (type.IsMimeType ("text", "*")) { - var text = new BodyPartText (); - text.Lines = ReadNumber (engine, format, cancellationToken); + var text = new BodyPartText (type, path) { + Lines = await ReadNumberAsync (engine, format, cancellationToken).ConfigureAwait (false) + }; body = text; } else { - body = new BodyPartBasic (); + isMultipart = type.IsMimeType ("multipart", "*"); + body = new BodyPartBasic (type, path); } body.ContentTransferEncoding = enc; body.ContentDescription = desc; - body.PartSpecifier = path; - body.ContentType = type; body.ContentId = id; body.Octets = octets; // if we are parsing a BODYSTRUCTURE, we may get some more tokens before the ')' - token = engine.PeekToken (cancellationToken); + token = await engine.PeekTokenAsync (cancellationToken).ConfigureAwait (false); - if (token.Type != ImapTokenType.CloseParen) { - body.ContentMd5 = ReadNStringToken (engine, format, false, cancellationToken); - token = engine.PeekToken (cancellationToken); - } + if (!isMultipart) { + if (token.Type != ImapTokenType.CloseParen) { + body.ContentMd5 = await ReadNStringTokenAsync (engine, format, false, cancellationToken).ConfigureAwait (false); + token = await engine.PeekTokenAsync (cancellationToken).ConfigureAwait (false); + } - if (token.Type != ImapTokenType.CloseParen) { - body.ContentDisposition = ParseContentDisposition (engine, format, cancellationToken); - token = engine.PeekToken (cancellationToken); - } + if (token.Type != ImapTokenType.CloseParen) { + body.ContentDisposition = await ParseContentDispositionAsync (engine, format, cancellationToken).ConfigureAwait (false); + token = await engine.PeekTokenAsync (cancellationToken).ConfigureAwait (false); + } - if (token.Type != ImapTokenType.CloseParen) { - body.ContentLanguage = ParseContentLanguage (engine, format, cancellationToken); - token = engine.PeekToken (cancellationToken); - } + if (token.Type != ImapTokenType.CloseParen) { + body.ContentLanguage = await ParseContentLanguageAsync (engine, format, cancellationToken).ConfigureAwait (false); + token = await engine.PeekTokenAsync (cancellationToken).ConfigureAwait (false); + } - if (token.Type != ImapTokenType.CloseParen) { - body.ContentLocation = ParseContentLocation (engine, format, cancellationToken); - token = engine.PeekToken (cancellationToken); + if (token.Type != ImapTokenType.CloseParen && token.Type != ImapTokenType.OpenParen) { + body.ContentLocation = await ParseContentLocationAsync (engine, format, cancellationToken).ConfigureAwait (false); + token = await engine.PeekTokenAsync (cancellationToken).ConfigureAwait (false); + } } - if (token.Type != ImapTokenType.CloseParen) - SkipBodyExtensions (engine, format, cancellationToken); + while (token.Type != ImapTokenType.CloseParen) { + await SkipBodyExtensionAsync (engine, format, cancellationToken).ConfigureAwait (false); + token = await engine.PeekTokenAsync (cancellationToken).ConfigureAwait (false); + } // read the ')' - token = engine.ReadToken (cancellationToken); - - if (token.Type != ImapTokenType.CloseParen) - throw ImapEngine.UnexpectedToken (format, token); + await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); return body; } - struct EnvelopeAddress + readonly struct EnvelopeAddress { - public readonly string Name; - public readonly string Route; - public readonly string Mailbox; - public readonly string Domain; + public readonly string? Name; + public readonly string? Route; + public readonly string? Mailbox; + public readonly string? Domain; - public EnvelopeAddress (string[] values) + public EnvelopeAddress (string?[] values) { Name = values[0]; Route = values[1]; @@ -956,93 +2128,192 @@ public EnvelopeAddress (string[] values) } public bool IsGroupStart { - get { return Domain == null; } + get { return Name == null && Route == null && Mailbox != null && Domain == null; } } public bool IsGroupEnd { - get { return Mailbox == null; } + get { return Name == null && Route == null && Mailbox == null && Domain == null; } } - public MailboxAddress ToMailboxAddress () + public MailboxAddress ToMailboxAddress (ImapEngine engine) { + if (engine.QuirksMode == ImapQuirksMode.GMail && Name != null && Name[0] == '<' && Name[Name.Length - 1] == '>' && Mailbox != null && Domain == null) { + // For whatever reason, GMail seems to sometimes break by reversing the Name and Mailbox tokens. + // For an example, see the second error report in https://github.com/jstedfast/MailKit/issues/494 + // where the Sender: address in the ENVELOPE has the name and address tokens flipped. + // + // Another example can be seen in https://github.com/jstedfast/MailKit/pull/1319. + var reversed = string.Format ("{0} {1}", Mailbox, Name); + + try { + return MailboxAddress.Parse (reversed); + } catch (ParseException) { + // fall through to normal processing + } + } + var mailbox = Mailbox; var domain = Domain; - string name = null; + string? name = null; + string address; - if (Name != null) { - // Note: since the ImapEngine.ReadLiteral() uses iso-8859-1 - // to convert bytes to unicode, we can undo that here: - name = Rfc2047.DecodePhrase (ImapEngine.Latin1.GetBytes (Name)); - } + if (Name != null) + name = Rfc2047.DecodePhrase (TextEncodings.UTF8.GetBytes (Name)); // Note: When parsing mailbox addresses w/o a domain, Dovecot will // use "MISSING_DOMAIN" as the domain string to prevent it from // appearing as a group address in the IMAP ENVELOPE response. - if (domain == "MISSING_DOMAIN") + if (domain == "MISSING_DOMAIN" || domain == ".MISSING-HOST-NAME.") domain = null; else if (domain != null) domain = domain.TrimEnd ('>'); - if (mailbox != null) + if (mailbox != null) { mailbox = mailbox.TrimStart ('<'); - string address = domain != null ? mailbox + "@" + domain : Mailbox; - DomainList route; + address = domain != null ? mailbox + "@" + domain : mailbox; + } else { + address = string.Empty; + } - if (Route != null && DomainList.TryParse (Route, out route)) + if (Route != null && DomainList.TryParse (Route, out var route)) return new MailboxAddress (name, route, address); return new MailboxAddress (name, address); } - public GroupAddress ToGroupAddress () + public GroupAddress ToGroupAddress (ImapEngine engine) { var name = string.Empty; - if (Mailbox != null) { - // Note: since the ImapEngine.ReadLiteral() uses iso-8859-1 - // to convert bytes to unicode, we can undo that here: - name = Rfc2047.DecodePhrase (ImapEngine.Latin1.GetBytes (Mailbox)); - } + if (Mailbox != null) + name = Rfc2047.DecodePhrase (TextEncodings.UTF8.GetBytes (Mailbox)); return new GroupAddress (name); } } + static bool TryAddEnvelopeAddressToken (ImapToken token, ref int index, string?[] values, bool[] qstrings, string format) + { + // This is a work-around for mail servers which output too many tokens for an ENVELOPE address. In at least 1 case, this happened + // because the server sent a literal token as the name component and miscalculated the literal length as 38 when it was actually 69 + // (likely using Unicode characters instead of UTF-8 bytes). + // + // The work-around is to keep merging tokens at the beginning of the list until we end up with only 4 tokens. + // + // See https://github.com/jstedfast/MailKit/issues/1369 for details. + if (index >= 4) { + if (qstrings[0]) + values[0] = MimeUtils.Quote (values[0]!); + if (qstrings[1]) + values[1] = MimeUtils.Quote (values[1]!); + values[0] = values[0] + ' ' + values[1]; + qstrings[0] = false; + qstrings[1] = qstrings[2]; + values[1] = values[2]; + qstrings[2] = qstrings[3]; + values[2] = values[3]; + index = 3; + } + + switch (token.Type) { + case ImapTokenType.Literal: + // Return control to our caller so that it can read the literal token. + qstrings[index] = false; + return false; + case ImapTokenType.QString: + values[index] = (string) token.Value; + qstrings[index] = true; + break; + case ImapTokenType.Atom: + values[index] = (string) token.Value; + qstrings[index] = false; + break; + case ImapTokenType.Nil: + values[index] = null; + qstrings[index] = false; + break; + default: + throw ImapEngine.UnexpectedToken (format, token); + } + + return true; + } + static EnvelopeAddress ParseEnvelopeAddress (ImapEngine engine, string format, CancellationToken cancellationToken) { - var values = new string[4]; + var values = new string?[4]; + var qstrings = new bool[4]; ImapToken token; int index = 0; do { token = engine.ReadToken (cancellationToken); - switch (token.Type) { - case ImapTokenType.Literal: - values[index] = engine.ReadLiteral (cancellationToken); - break; - case ImapTokenType.QString: - case ImapTokenType.Atom: - values[index] = (string) token.Value; - break; - case ImapTokenType.Nil: + if (token.Type == ImapTokenType.CloseParen) break; - default: - throw ImapEngine.UnexpectedToken (format, token); - } + + if (!TryAddEnvelopeAddressToken (token, ref index, values, qstrings, format)) + values[index] = engine.ReadLiteral (cancellationToken); index++; - } while (index < 4); + } while (true); - token = engine.ReadToken (cancellationToken); + ImapEngine.AssertToken (token, ImapTokenType.CloseParen, format, token); - if (token.Type != ImapTokenType.CloseParen) - throw ImapEngine.UnexpectedToken (format, token); + return new EnvelopeAddress (values); + } + + static async Task ParseEnvelopeAddressAsync (ImapEngine engine, string format, CancellationToken cancellationToken) + { + var values = new string?[4]; + var qstrings = new bool[4]; + ImapToken token; + int index = 0; + + do { + token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.CloseParen) + break; + + if (!TryAddEnvelopeAddressToken (token, ref index, values, qstrings, format)) + values[index] = await engine.ReadLiteralAsync (cancellationToken).ConfigureAwait (false); + + index++; + } while (true); + + ImapEngine.AssertToken (token, ImapTokenType.CloseParen, format, token); return new EnvelopeAddress (values); } + static void AddEnvelopeAddress (ImapEngine engine, List stack, ref int sp, EnvelopeAddress address) + { + if (address.IsGroupStart && engine.QuirksMode != ImapQuirksMode.GMail) { + var group = address.ToGroupAddress (engine); + stack[sp].Add (group); + stack.Add (group.Members); + sp++; + } else if (address.IsGroupEnd) { + if (sp > 0) { + stack.RemoveAt (sp); + sp--; + } + } else { + try { + // Note: We need to do a try/catch around ToMailboxAddress() because some addresses + // returned by the IMAP server might be completely horked. For an example, see the + // second error report in https://github.com/jstedfast/MailKit/issues/494 where one + // of the addresses in the ENVELOPE has the name and address tokens flipped. + var mailbox = address.ToMailboxAddress (engine); + stack[sp].Add (mailbox); + } catch { + return; + } + } + } + static void ParseEnvelopeAddressList (InternetAddressList list, ImapEngine engine, string format, CancellationToken cancellationToken) { var token = engine.ReadToken (cancellationToken); @@ -1050,10 +2321,12 @@ static void ParseEnvelopeAddressList (InternetAddressList list, ImapEngine engin if (token.Type == ImapTokenType.Nil) return; - if (token.Type != ImapTokenType.OpenParen) - throw ImapEngine.UnexpectedToken (format, token); + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, format, token); + + var stack = new List (); + int sp = 0; - GroupAddress group = null; + stack.Add (list); do { token = engine.ReadToken (cancellationToken); @@ -1061,41 +2334,55 @@ static void ParseEnvelopeAddressList (InternetAddressList list, ImapEngine engin if (token.Type == ImapTokenType.CloseParen) break; - if (token.Type != ImapTokenType.OpenParen) - throw ImapEngine.UnexpectedToken (format, token); + // Note: As seen in https://github.com/jstedfast/MailKit/issues/991, it seems that SmarterMail IMAP + // servers will sometimes include a NIL address token within the address list. Just ignore it. + if (token.Type == ImapTokenType.Nil) + continue; + + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, format, token); + + var address = ParseEnvelopeAddress (engine, format, cancellationToken); + + AddEnvelopeAddress (engine, stack, ref sp, address); + } while (true); + } + + static async Task ParseEnvelopeAddressListAsync (InternetAddressList list, ImapEngine engine, string format, CancellationToken cancellationToken) + { + var token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.Nil) + return; + + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, format, token); + + var stack = new List (); + int sp = 0; + + stack.Add (list); + + do { + token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.CloseParen) + break; - var item = ParseEnvelopeAddress (engine, format, cancellationToken); + // Note: As seen in https://github.com/jstedfast/MailKit/issues/991, it seems that SmarterMail IMAP + // servers will sometimes include a NIL address token within the address list. Just ignore it. + if (token.Type == ImapTokenType.Nil) + continue; - if (item.IsGroupStart && !engine.IsGMail && group == null) { - group = item.ToGroupAddress (); - list.Add (group); - } else if (item.IsGroupEnd) { - group = null; - } else { - MailboxAddress mailbox; + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, format, token); - try { - // Note: We need to do a try/catch around ToMailboxAddress() because some addresses - // returned by the IMAP server might be completely horked. For an example, see the - // second error report in https://github.com/jstedfast/MailKit/issues/494 where one - // of the addresses in the ENVELOPE has the name and address tokens flipped. - mailbox = item.ToMailboxAddress (); - } catch { - continue; - } + var address = await ParseEnvelopeAddressAsync (engine, format, cancellationToken).ConfigureAwait (false); - if (group != null) - group.Members.Add (mailbox); - else - list.Add (mailbox); - } + AddEnvelopeAddress (engine, stack, ref sp, address); } while (true); } static DateTimeOffset? ParseEnvelopeDate (ImapEngine engine, string format, CancellationToken cancellationToken) { var token = engine.ReadToken (cancellationToken); - DateTimeOffset date; string value; switch (token.Type) { @@ -1112,7 +2399,32 @@ static void ParseEnvelopeAddressList (InternetAddressList list, ImapEngine engin throw ImapEngine.UnexpectedToken (format, token); } - if (!DateUtils.TryParse (value, out date)) + if (!DateUtils.TryParse (value, out var date)) + return null; + + return date; + } + + static async Task ParseEnvelopeDateAsync (ImapEngine engine, string format, CancellationToken cancellationToken) + { + var token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + string value; + + switch (token.Type) { + case ImapTokenType.Literal: + value = await engine.ReadLiteralAsync (cancellationToken).ConfigureAwait (false); + break; + case ImapTokenType.QString: + case ImapTokenType.Atom: + value = (string) token.Value; + break; + case ImapTokenType.Nil: + return null; + default: + throw ImapEngine.UnexpectedToken (format, token); + } + + if (!DateUtils.TryParse (value, out var date)) return null; return date; @@ -1128,10 +2440,9 @@ public static Envelope ParseEnvelope (ImapEngine engine, CancellationToken cance { string format = string.Format (ImapEngine.GenericItemSyntaxErrorFormat, "ENVELOPE", "{0}"); var token = engine.ReadToken (cancellationToken); - string nstring; + string? nstring; - if (token.Type != ImapTokenType.OpenParen) - throw ImapEngine.UnexpectedToken (format, token); + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, format, token); var envelope = new Envelope (); envelope.Date = ParseEnvelopeDate (engine, format, cancellationToken); @@ -1143,16 +2454,86 @@ public static Envelope ParseEnvelope (ImapEngine engine, CancellationToken cance ParseEnvelopeAddressList (envelope.Cc, engine, format, cancellationToken); ParseEnvelopeAddressList (envelope.Bcc, engine, format, cancellationToken); - if ((nstring = ReadNStringToken (engine, format, false, cancellationToken)) != null) - envelope.InReplyTo = MimeUtils.EnumerateReferences (nstring).FirstOrDefault (); + // Note: Some broken IMAP servers will forget to include the In-Reply-To token (I guess if the header isn't set?). + // + // See https://github.com/jstedfast/MailKit/issues/932 + token = engine.PeekToken (cancellationToken); + if (token.Type != ImapTokenType.CloseParen) { + if ((nstring = ReadNStringToken (engine, format, false, cancellationToken)) != null) + envelope.InReplyTo = MimeUtils.EnumerateReferences (nstring).FirstOrDefault (); - if ((nstring = ReadNStringToken (engine, format, false, cancellationToken)) != null) - envelope.MessageId = MimeUtils.ParseMessageId (nstring); + // Note: Some broken IMAP servers will forget to include the Message-Id token (I guess if the header isn't set?). + // + // See https://github.com/jstedfast/MailKit/issues/669 + token = engine.PeekToken (cancellationToken); + if (token.Type != ImapTokenType.CloseParen) { + if ((nstring = ReadNStringToken (engine, format, false, cancellationToken)) != null) { + try { + envelope.MessageId = MimeUtils.ParseMessageId (nstring); + } catch { + envelope.MessageId = nstring; + } + } + } + } token = engine.ReadToken (cancellationToken); - if (token.Type != ImapTokenType.CloseParen) - throw ImapEngine.UnexpectedToken (format, token); + ImapEngine.AssertToken (token, ImapTokenType.CloseParen, format, token); + + return envelope; + } + + /// + /// Parses the ENVELOPE parenthesized list. + /// + /// The envelope. + /// The IMAP engine. + /// The cancellation token. + public static async Task ParseEnvelopeAsync (ImapEngine engine, CancellationToken cancellationToken) + { + string format = string.Format (ImapEngine.GenericItemSyntaxErrorFormat, "ENVELOPE", "{0}"); + var token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + string? nstring; + + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, format, token); + + var envelope = new Envelope (); + envelope.Date = await ParseEnvelopeDateAsync (engine, format, cancellationToken).ConfigureAwait (false); + envelope.Subject = await ReadNStringTokenAsync (engine, format, true, cancellationToken).ConfigureAwait (false); + await ParseEnvelopeAddressListAsync (envelope.From, engine, format, cancellationToken).ConfigureAwait (false); + await ParseEnvelopeAddressListAsync (envelope.Sender, engine, format, cancellationToken).ConfigureAwait (false); + await ParseEnvelopeAddressListAsync (envelope.ReplyTo, engine, format, cancellationToken).ConfigureAwait (false); + await ParseEnvelopeAddressListAsync (envelope.To, engine, format, cancellationToken).ConfigureAwait (false); + await ParseEnvelopeAddressListAsync (envelope.Cc, engine, format, cancellationToken).ConfigureAwait (false); + await ParseEnvelopeAddressListAsync (envelope.Bcc, engine, format, cancellationToken).ConfigureAwait (false); + + // Note: Some broken IMAP servers will forget to include the In-Reply-To token (I guess if the header isn't set?). + // + // See https://github.com/jstedfast/MailKit/issues/932 + token = await engine.PeekTokenAsync (cancellationToken).ConfigureAwait (false); + if (token.Type != ImapTokenType.CloseParen) { + if ((nstring = await ReadNStringTokenAsync (engine, format, false, cancellationToken).ConfigureAwait (false)) != null) + envelope.InReplyTo = MimeUtils.EnumerateReferences (nstring).FirstOrDefault (); + + // Note: Some broken IMAP servers will forget to include the Message-Id token (I guess if the header isn't set?). + // + // See https://github.com/jstedfast/MailKit/issues/669 + token = await engine.PeekTokenAsync (cancellationToken).ConfigureAwait (false); + if (token.Type != ImapTokenType.CloseParen) { + if ((nstring = await ReadNStringTokenAsync (engine, format, false, cancellationToken).ConfigureAwait (false)) != null) { + try { + envelope.MessageId = MimeUtils.ParseMessageId (nstring); + } catch { + envelope.MessageId = nstring; + } + } + } + } + + token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + ImapEngine.AssertToken (token, ImapTokenType.CloseParen, format, token); return envelope; } @@ -1160,13 +2541,11 @@ public static Envelope ParseEnvelope (ImapEngine engine, CancellationToken cance /// /// Formats a flags list suitable for use with the APPEND command. /// - /// The flags list string. /// The message flags. - /// The number of user-defined flags. - public static string FormatFlagsList (MessageFlags flags, int numUserFlags) + /// The string builder. + /// The number of keywords. + public static void FormatFlagsList (StringBuilder builder, MessageFlags flags, int numKeywords) { - var builder = new StringBuilder (); - builder.Append ('('); if ((flags & MessageFlags.Answered) != 0) @@ -1180,66 +2559,226 @@ public static string FormatFlagsList (MessageFlags flags, int numUserFlags) if ((flags & MessageFlags.Seen) != 0) builder.Append ("\\Seen "); - for (int i = 0; i < numUserFlags; i++) + for (int i = 0; i < numKeywords; i++) builder.Append ("%S "); if (builder.Length > 1) builder.Length--; builder.Append (')'); + } + + /// + /// Formats a flags list suitable for use with the APPEND command. + /// + /// The flags list string. + /// The message flags. + /// The number of keywords. + public static string FormatFlagsList (MessageFlags flags, int numKeywords) + { + var builder = new StringBuilder (); + + FormatFlagsList (builder, flags, numKeywords); return builder.ToString (); } + static void AddFlag (ImapToken token, ref MessageFlags flags, HashSet keywords) + { + if (token.Type != ImapTokenType.Nil) { + var flag = (string) token.Value; + + if (flag.Equals ("\\Answered", StringComparison.OrdinalIgnoreCase)) + flags |= MessageFlags.Answered; + else if (flag.Equals ("\\Deleted", StringComparison.OrdinalIgnoreCase)) + flags |= MessageFlags.Deleted; + else if (flag.Equals ("\\Draft", StringComparison.OrdinalIgnoreCase)) + flags |= MessageFlags.Draft; + else if (flag.Equals ("\\Flagged", StringComparison.OrdinalIgnoreCase)) + flags |= MessageFlags.Flagged; + else if (flag.Equals ("\\Seen", StringComparison.OrdinalIgnoreCase)) + flags |= MessageFlags.Seen; + else if (flag.Equals ("\\Recent", StringComparison.OrdinalIgnoreCase)) + flags |= MessageFlags.Recent; + else if (flag.Equals ("\\*", StringComparison.OrdinalIgnoreCase)) + flags |= MessageFlags.UserDefined; + else if (keywords != null) + keywords.Add (flag); + } + } + /// /// Parses the flags list. /// /// The message flags. /// The IMAP engine. /// The name of the flags being parsed. - /// A hash set of user-defined message flags that will be populated if non-null. + /// A hash set of user-defined message flags that will be populated if non-null. /// The cancellation token. - public static MessageFlags ParseFlagsList (ImapEngine engine, string name, HashSet userFlags, CancellationToken cancellationToken) + public static MessageFlags ParseFlagsList (ImapEngine engine, string name, HashSet keywords, CancellationToken cancellationToken) { - var specials = engine.IsGMail ? ImapStream.GMailLabelSpecials : ImapStream.AtomSpecials; var token = engine.ReadToken (cancellationToken); var flags = MessageFlags.None; - if (token.Type != ImapTokenType.OpenParen) { - Debug.WriteLine ("Expected '(' at the start of the {0} list, but got: {1}", name, token); - throw ImapEngine.UnexpectedToken (ImapEngine.GenericItemSyntaxErrorFormat, name, token); + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, ImapEngine.GenericItemSyntaxErrorFormat, name, token); + + token = engine.ReadToken (ImapStream.AtomSpecials, cancellationToken); + + while (token.Type == ImapTokenType.Atom || token.Type == ImapTokenType.Flag || token.Type == ImapTokenType.QString || token.Type == ImapTokenType.Nil) { + AddFlag (token, ref flags, keywords); + + token = engine.ReadToken (ImapStream.AtomSpecials, cancellationToken); } - token = engine.ReadToken (specials, cancellationToken); + ImapEngine.AssertToken (token, ImapTokenType.CloseParen, ImapEngine.GenericItemSyntaxErrorFormat, name, token); - while (token.Type == ImapTokenType.Atom || token.Type == ImapTokenType.Flag) { - var flag = (string) token.Value; + return flags; + } - switch (flag) { - case "\\Answered": flags |= MessageFlags.Answered; break; - case "\\Deleted": flags |= MessageFlags.Deleted; break; - case "\\Draft": flags |= MessageFlags.Draft; break; - case "\\Flagged": flags |= MessageFlags.Flagged; break; - case "\\Seen": flags |= MessageFlags.Seen; break; - case "\\Recent": flags |= MessageFlags.Recent; break; - case "\\*": flags |= MessageFlags.UserDefined; break; - default: - if (userFlags != null) - userFlags.Add (flag); - break; - } + /// + /// Parses the flags list. + /// + /// The message flags. + /// The IMAP engine. + /// The name of the flags being parsed. + /// A hash set of user-defined message flags that will be populated if non-null. + /// The cancellation token. + public static async Task ParseFlagsListAsync (ImapEngine engine, string name, HashSet keywords, CancellationToken cancellationToken) + { + var token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + var flags = MessageFlags.None; - token = engine.ReadToken (specials, cancellationToken); - } + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, ImapEngine.GenericItemSyntaxErrorFormat, name, token); - if (token.Type != ImapTokenType.CloseParen) { - Debug.WriteLine ("Expected to find a ')' token terminating the {0} list, but got: {1}", name, token); - throw ImapEngine.UnexpectedToken (ImapEngine.GenericItemSyntaxErrorFormat, name, token); + token = await engine.ReadTokenAsync (ImapStream.AtomSpecials, cancellationToken).ConfigureAwait (false); + + while (token.Type == ImapTokenType.Atom || token.Type == ImapTokenType.Flag || token.Type == ImapTokenType.QString || token.Type == ImapTokenType.Nil) { + AddFlag (token, ref flags, keywords); + + token = await engine.ReadTokenAsync (ImapStream.AtomSpecials, cancellationToken).ConfigureAwait (false); } + ImapEngine.AssertToken (token, ImapTokenType.CloseParen, ImapEngine.GenericItemSyntaxErrorFormat, name, token); + return flags; } + /// + /// Parses the ANNOTATION list. + /// + /// The list of annotations. + /// The IMAP engine. + /// The cancellation token. + public static ReadOnlyCollection ParseAnnotations (ImapEngine engine, CancellationToken cancellationToken) + { + var format = string.Format (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ANNOTATION", "{0}"); + var token = engine.ReadToken (cancellationToken); + var annotations = new List (); + + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, ImapEngine.GenericItemSyntaxErrorFormat, "ANNOTATION", token); + + do { + token = engine.PeekToken (ImapStream.AtomSpecials, cancellationToken); + + if (token.Type == ImapTokenType.CloseParen) + break; + + var path = ReadStringToken (engine, format, cancellationToken); + var entry = AnnotationEntry.Parse (path); + var annotation = new Annotation (entry); + + annotations.Add (annotation); + + token = engine.PeekToken (cancellationToken); + + // Note: Unsolicited FETCH responses that include ANNOTATION data do not include attribute values. + if (token.Type == ImapTokenType.OpenParen) { + // consume the '(' + engine.ReadToken (cancellationToken); + + // read the attribute/value pairs + do { + token = engine.PeekToken (ImapStream.AtomSpecials, cancellationToken); + + if (token.Type == ImapTokenType.CloseParen) + break; + + var name = ReadStringToken (engine, format, cancellationToken); + var value = ReadNStringToken (engine, format, false, cancellationToken); + var attribute = new AnnotationAttribute (name); + + annotation.Properties[attribute] = value; + } while (true); + + // consume the ')' + engine.ReadToken (cancellationToken); + } + } while (true); + + // consume the ')' + engine.ReadToken (cancellationToken); + + return new ReadOnlyCollection (annotations); + } + + /// + /// Parses the ANNOTATION list. + /// + /// The list of annotations. + /// The IMAP engine. + /// The cancellation token. + public static async Task> ParseAnnotationsAsync (ImapEngine engine, CancellationToken cancellationToken) + { + var format = string.Format (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ANNOTATION", "{0}"); + var token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + var annotations = new List (); + + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, ImapEngine.GenericItemSyntaxErrorFormat, "ANNOTATION", token); + + do { + token = await engine.PeekTokenAsync (ImapStream.AtomSpecials, cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.CloseParen) + break; + + var path = await ReadStringTokenAsync (engine, format, cancellationToken).ConfigureAwait (false); + var entry = AnnotationEntry.Parse (path); + var annotation = new Annotation (entry); + + annotations.Add (annotation); + + token = await engine.PeekTokenAsync (cancellationToken).ConfigureAwait (false); + + // Note: Unsolicited FETCH responses that include ANNOTATION data do not include attribute values. + if (token.Type == ImapTokenType.OpenParen) { + // consume the '(' + await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + // read the attribute/value pairs + do { + token = await engine.PeekTokenAsync (ImapStream.AtomSpecials, cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.CloseParen) + break; + + var name = await ReadStringTokenAsync (engine, format, cancellationToken).ConfigureAwait (false); + var value = await ReadNStringTokenAsync (engine, format, false, cancellationToken).ConfigureAwait (false); + var attribute = new AnnotationAttribute (name); + + annotation.Properties[attribute] = value; + } while (true); + + // consume the ')' + await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + } + } while (true); + + // consume the ')' + await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + return new ReadOnlyCollection (annotations); + } + /// /// Parses the X-GM-LABELS list. /// @@ -1251,11 +2790,9 @@ public static ReadOnlyCollection ParseLabelsList (ImapEngine engine, Can var token = engine.ReadToken (cancellationToken); var labels = new List (); - if (token.Type != ImapTokenType.OpenParen) - throw ImapEngine.UnexpectedToken (ImapEngine.GenericItemSyntaxErrorFormat, "X-GM-LABELS", token); + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, ImapEngine.GenericItemSyntaxErrorFormat, "X-GM-LABELS", token); - // Note: GMail's IMAP implementation is broken and does not quote strings with ']' like it should. - token = engine.ReadToken (ImapStream.GMailLabelSpecials, cancellationToken); + token = engine.ReadToken (ImapStream.AtomSpecials, cancellationToken); while (token.Type == ImapTokenType.Flag || token.Type == ImapTokenType.Atom || token.Type == ImapTokenType.QString || token.Type == ImapTokenType.Nil) { // Apparently it's possible to set a NIL label in GMail... @@ -1266,14 +2803,48 @@ public static ReadOnlyCollection ParseLabelsList (ImapEngine engine, Can labels.Add (label); } else { - labels.Add ("NIL"); + labels.Add ((string) token.Value); } - token = engine.ReadToken (ImapStream.GMailLabelSpecials, cancellationToken); + token = engine.ReadToken (ImapStream.AtomSpecials, cancellationToken); } - if (token.Type != ImapTokenType.CloseParen) - throw ImapEngine.UnexpectedToken (ImapEngine.GenericItemSyntaxErrorFormat, "X-GM-LABELS", token); + ImapEngine.AssertToken (token, ImapTokenType.CloseParen, ImapEngine.GenericItemSyntaxErrorFormat, "X-GM-LABELS", token); + + return new ReadOnlyCollection (labels); + } + + /// + /// Parses the X-GM-LABELS list. + /// + /// The message labels. + /// The IMAP engine. + /// The cancellation token. + public static async Task> ParseLabelsListAsync (ImapEngine engine, CancellationToken cancellationToken) + { + var token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + var labels = new List (); + + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, ImapEngine.GenericItemSyntaxErrorFormat, "X-GM-LABELS", token); + + token = await engine.ReadTokenAsync (ImapStream.AtomSpecials, cancellationToken).ConfigureAwait (false); + + while (token.Type == ImapTokenType.Flag || token.Type == ImapTokenType.Atom || token.Type == ImapTokenType.QString || token.Type == ImapTokenType.Nil) { + // Apparently it's possible to set a NIL label in GMail... + // + // See https://github.com/jstedfast/MailKit/issues/244 for an example. + if (token.Type != ImapTokenType.Nil) { + var label = engine.DecodeMailboxName ((string) token.Value); + + labels.Add (label); + } else { + labels.Add ((string) token.Value); + } + + token = await engine.ReadTokenAsync (ImapStream.AtomSpecials, cancellationToken).ConfigureAwait (false); + } + + ImapEngine.AssertToken (token, ImapTokenType.CloseParen, ImapEngine.GenericItemSyntaxErrorFormat, "X-GM-LABELS", token); return new ReadOnlyCollection (labels); } @@ -1284,9 +2855,20 @@ static MessageThread ParseThread (ImapEngine engine, uint uidValidity, Cancellat MessageThread thread, node, child; uint uid; - if (token.Type != ImapTokenType.Atom || !uint.TryParse ((string) token.Value, out uid) || uid == 0) - throw ImapEngine.UnexpectedToken (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "THREAD", token); + if (token.Type == ImapTokenType.OpenParen) { + thread = new MessageThread ((UniqueId?) null /*UniqueId.Invalid*/); + + do { + child = ParseThread (engine, uidValidity, cancellationToken); + thread.Children.Add (child); + + token = engine.ReadToken (cancellationToken); + } while (token.Type != ImapTokenType.CloseParen); + + return thread; + } + uid = ImapEngine.ParseNumber (token, true, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "THREAD", token); node = thread = new MessageThread (new UniqueId (uidValidity, uid)); do { @@ -1299,9 +2881,49 @@ static MessageThread ParseThread (ImapEngine engine, uint uidValidity, Cancellat child = ParseThread (engine, uidValidity, cancellationToken); node.Children.Add (child); } else { - if (token.Type != ImapTokenType.Atom || !uint.TryParse ((string) token.Value, out uid) || uid == 0) - throw ImapEngine.UnexpectedToken (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "THREAD", token); + uid = ImapEngine.ParseNumber (token, true, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "THREAD", token); + child = new MessageThread (new UniqueId (uidValidity, uid)); + node.Children.Add (child); + node = child; + } + } while (true); + + return thread; + } + + static async Task ParseThreadAsync (ImapEngine engine, uint uidValidity, CancellationToken cancellationToken) + { + var token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + MessageThread thread, node, child; + uint uid; + + if (token.Type == ImapTokenType.OpenParen) { + thread = new MessageThread ((UniqueId?) null /*UniqueId.Invalid*/); + + do { + child = await ParseThreadAsync (engine, uidValidity, cancellationToken).ConfigureAwait (false); + thread.Children.Add (child); + + token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + } while (token.Type != ImapTokenType.CloseParen); + + return thread; + } + + uid = ImapEngine.ParseNumber (token, true, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "THREAD", token); + node = thread = new MessageThread (new UniqueId (uidValidity, uid)); + do { + token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.CloseParen) + break; + + if (token.Type == ImapTokenType.OpenParen) { + child = await ParseThreadAsync (engine, uidValidity, cancellationToken).ConfigureAwait (false); + node.Children.Add (child); + } else { + uid = ImapEngine.ParseNumber (token, true, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "THREAD", token); child = new MessageThread (new UniqueId (uidValidity, uid)); node.Children.Add (child); node = child; @@ -1312,15 +2934,15 @@ static MessageThread ParseThread (ImapEngine engine, uint uidValidity, Cancellat } /// - /// Parses the threads. + /// Parses an untagged THREAD response. /// - /// The threads. + /// The task. /// The IMAP engine. /// The UIDVALIDITY of the folder. + /// The list of message threads that this method will append to. /// The cancellation token. - public static IList ParseThreads (ImapEngine engine, uint uidValidity, CancellationToken cancellationToken) + public static void ParseThreads (ImapEngine engine, uint uidValidity, List threads, CancellationToken cancellationToken) { - var threads = new List (); ImapToken token; do { @@ -1331,13 +2953,59 @@ public static IList ParseThreads (ImapEngine engine, uint uidVali token = engine.ReadToken (cancellationToken); - if (token.Type != ImapTokenType.OpenParen) - throw ImapEngine.UnexpectedToken (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "THREAD", token); + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "THREAD", token); threads.Add (ParseThread (engine, uidValidity, cancellationToken)); } while (true); + } + + /// + /// Parses the threads. + /// + /// The task. + /// The IMAP engine. + /// The UIDVALIDITY of the folder. + /// THe list of message threads that will be appended to. + /// The cancellation token. + public static async Task ParseThreadsAsync (ImapEngine engine, uint uidValidity, List threads, CancellationToken cancellationToken) + { + ImapToken token; + + do { + token = await engine.PeekTokenAsync (cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.Eoln) + break; + + token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "THREAD", token); + + threads.Add (await ParseThreadAsync (engine, uidValidity, cancellationToken).ConfigureAwait (false)); + } while (true); + } + + /// + /// Handles an untagged THREAD response. + /// + /// The task. + /// The IMAP engine. + /// The IMAP command. + /// THe index. + /// Whether or not asynchronous IO methods should be used. + public static Task UntaggedThreadHandler (ImapEngine engine, ImapCommand ic, int index, bool doAsync) + { + var threads = new List (); + var folder = ic.Folder!; + + ic.UserData = threads; + + if (doAsync) + return ParseThreadsAsync (engine, folder.UidValidity, threads, ic.CancellationToken); + + ParseThreads (engine, folder.UidValidity, threads, ic.CancellationToken); - return threads; + return Task.CompletedTask; } } } diff --git a/MailKit/Net/NetworkOperation.cs b/MailKit/Net/NetworkOperation.cs new file mode 100644 index 0000000000..58f3520aff --- /dev/null +++ b/MailKit/Net/NetworkOperation.cs @@ -0,0 +1,141 @@ +// +// NetworkOperation.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Diagnostics; + +namespace MailKit.Net { + enum NetworkOperationKind + { + Authenticate, + Connect, + Send + } + + class NetworkOperation : IDisposable + { +#if NET6_0_OR_GREATER + static readonly string[] ActivityNames = { + "Authenticate", + "Connect", + "Send" + }; + static readonly string[] OperationValues = { + "authenticate", + "connect", + "send" + }; + + readonly NetworkOperationKind kind; + readonly ClientMetrics? metrics; + readonly Activity? activity; + readonly long startTimestamp; + readonly Uri uri; + Exception? ex; + + NetworkOperation (NetworkOperationKind kind, Uri uri, Activity? activity, ClientMetrics? metrics) + { + this.kind = kind; + this.uri = uri; + this.activity = activity; + this.metrics = metrics; + + if (activity is not null) { + activity.AddTag ("url.scheme", uri.Scheme); + activity.AddTag ("server.address", uri.Host); + activity.AddTag ("server.port", uri.Port); + } + + startTimestamp = Stopwatch.GetTimestamp (); + } +#else + Exception? ex; + + NetworkOperation () + { + } +#endif + + public void SetError (Exception ex) + { + this.ex = ex; + } + +#if NET6_0_OR_GREATER + // TagList is a huge struct, so we avoid storing it in a field to reduce the amount we allocate on the heap. + TagList GetTags () + { + var tags = ClientMetrics.GetTags (uri, ex); + + tags.Add ("network.operation", OperationValues[(int) kind]); + + return tags; + } +#endif + + public void Dispose () + { +#if NET6_0_OR_GREATER + if (metrics is not null && (metrics.OperationCounter.Enabled || metrics.OperationDuration.Enabled)) { + var tags = GetTags (); + + if (metrics.OperationDuration.Enabled) { + var duration = TimeSpan.FromTicks (Stopwatch.GetTimestamp () - startTimestamp).TotalMilliseconds; + + metrics.OperationDuration.Record (duration, tags); + } + + if (metrics.OperationCounter.Enabled) + metrics.OperationCounter.Add (1, tags); + } + + if (activity is not null) { + if (ex is not null) + activity.SetStatus (ActivityStatusCode.Error); + else + activity.SetStatus (ActivityStatusCode.Ok); + + activity.Dispose (); + } +#endif + } + +#if NET6_0_OR_GREATER + public static NetworkOperation Start (NetworkOperationKind kind, Uri uri, ActivitySource source, ClientMetrics? metrics) + { + + var activity = source?.StartActivity (ActivityNames[(int) kind], ActivityKind.Client); + + return new NetworkOperation (kind, uri, activity, metrics); + } +#else + public static NetworkOperation Start (NetworkOperationKind kind, Uri uri) + { + return new NetworkOperation (); + } +#endif + } +} diff --git a/MailKit/Net/NetworkStream.cs b/MailKit/Net/NetworkStream.cs new file mode 100644 index 0000000000..0997192d5b --- /dev/null +++ b/MailKit/Net/NetworkStream.cs @@ -0,0 +1,310 @@ +// +// NetworkStream.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Threading; +using System.Net.Sockets; +using System.Threading.Tasks; + +namespace MailKit.Net +{ + class NetworkStream : Stream + { + SocketAsyncEventArgs? send; + SocketAsyncEventArgs? recv; + bool ownsSocket; + bool connected; + + public NetworkStream (Socket socket, bool ownsSocket) + { + send = new SocketAsyncEventArgs (); + send.Completed += AsyncOperationCompleted; + send.AcceptSocket = socket; + + recv = new SocketAsyncEventArgs (); + recv.Completed += AsyncOperationCompleted; + recv.AcceptSocket = socket; + + this.ownsSocket = ownsSocket; + connected = socket.Connected; + Socket = socket; + } + + public Socket Socket { + get; private set; + } + + public bool DataAvailable { + get { return connected && Socket.Available > 0; } + } + + public override bool CanRead { + get { return connected; } + } + + public override bool CanWrite { + get { return connected; } + } + + public override bool CanSeek { + get { return false; } + } + + public override bool CanTimeout { + get { return connected; } + } + + public override long Length { + get { throw new NotSupportedException (); } + } + + public override long Position { + get { throw new NotSupportedException (); } + set { throw new NotSupportedException (); } + } + + public override int ReadTimeout { + get { + int timeout = Socket.ReceiveTimeout; + + return timeout == 0 ? Timeout.Infinite : timeout; + } + set { + if (value <= 0 && value != Timeout.Infinite) + throw new ArgumentOutOfRangeException (nameof (value)); + + Socket.ReceiveTimeout = value; + } + } + + public override int WriteTimeout { + get { + int timeout = Socket.SendTimeout; + + return timeout == 0 ? Timeout.Infinite : timeout; + } + set { + if (value <= 0 && value != Timeout.Infinite) + throw new ArgumentOutOfRangeException (nameof (value)); + + Socket.SendTimeout = value; + } + } + + void AsyncOperationCompleted (object? sender, SocketAsyncEventArgs args) + { + var tcs = (TaskCompletionSource) args.UserToken!; + + if (args.SocketError == SocketError.Success) { + tcs.TrySetResult (true); + return; + } + + tcs.TrySetException (new SocketException ((int) args.SocketError)); + } + + void Cleanup () + { + if (send != null) { + send.Completed -= AsyncOperationCompleted; + send.AcceptSocket = null; + send.Dispose (); + send = null; + } + + if (recv != null) { + recv.Completed -= AsyncOperationCompleted; + recv.AcceptSocket = null; + recv.Dispose (); + recv = null; + } + } + + void Disconnect () + { + try { + Socket.Disconnect (false); + Socket.Dispose (); + } catch { + return; + } finally { + connected = false; + Cleanup (); + } + } + + public override int Read (byte[] buffer, int offset, int count) + { + try { + return Socket.Receive (buffer, offset, count, SocketFlags.None); + } catch (SocketException ex) { + throw new IOException (ex.Message, ex); + } + } + + public override async Task ReadAsync (byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested (); + + var tcs = new TaskCompletionSource (); + + // Capture the ReadTimeout so even if we get an exception and disconnect the socket, we still have it. + int readTimeout = ReadTimeout; + + using (var timeout = new CancellationTokenSource (readTimeout)) { + using (var linked = CancellationTokenSource.CreateLinkedTokenSource (cancellationToken, timeout.Token)) { + using (var registration = linked.Token.Register (() => tcs.TrySetCanceled (), false)) { + recv!.SetBuffer (buffer, offset, count); + recv.UserToken = tcs; + + if (!Socket.ReceiveAsync (recv)) + AsyncOperationCompleted (null, recv); + + try { + await tcs.Task.ConfigureAwait (false); + return recv.BytesTransferred; + } catch (OperationCanceledException ex) { + Disconnect (); + if (timeout.IsCancellationRequested) + throw new TimeoutException ($"Operation timed out after {readTimeout} milliseconds", ex); + throw; + } catch (Exception ex) { + Disconnect (); + if (ex is SocketException) + throw new IOException (ex.Message, ex); + throw; + } + } + } + } + } + + public override void Write (byte[] buffer, int offset, int count) + { + try { + Socket.Send (buffer, offset, count, SocketFlags.None); + } catch (SocketException ex) { + throw new IOException (ex.Message, ex); + } + } + + public override async Task WriteAsync (byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested (); + + var tcs = new TaskCompletionSource (); + + // Capture the WriteTimeout so even if we get an exception and disconnect the socket, we still have it. + int writeTimeout = WriteTimeout; + + using (var timeout = new CancellationTokenSource (writeTimeout)) { + using (var linked = CancellationTokenSource.CreateLinkedTokenSource (cancellationToken, timeout.Token)) { + using (var registration = linked.Token.Register (() => tcs.TrySetCanceled (), false)) { + send!.SetBuffer (buffer, offset, count); + send.UserToken = tcs; + + if (!Socket.SendAsync (send)) + AsyncOperationCompleted (null, send); + + try { + await tcs.Task.ConfigureAwait (false); + } catch (OperationCanceledException ex) { + Disconnect (); + if (timeout.IsCancellationRequested) + throw new TimeoutException ($"Operation timed out after {writeTimeout} milliseconds", ex); + throw; + } catch (Exception ex) { + Disconnect (); + if (ex is SocketException) + throw new IOException (ex.Message, ex); + throw; + } + } + } + } + } + + public override void Flush () + { + } + + public override Task FlushAsync (CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public override long Seek (long offset, SeekOrigin origin) + { + throw new NotSupportedException (); + } + + public override void SetLength (long value) + { + throw new NotSupportedException (); + } + + public static NetworkStream? Get (Stream stream) + { +#if !MAILKIT_LITE + if (stream is CompressedStream compressed) + stream = compressed.InnerStream; +#endif + + if (stream is ExtendedSslStream ssl) + stream = ssl.InnerStream; + + return stream as NetworkStream; + } + + public void Poll (SelectMode mode, CancellationToken cancellationToken) + { + if (!cancellationToken.CanBeCanceled) + return; + + do { + cancellationToken.ThrowIfCancellationRequested (); + // wait 1/4 second and then re-check for cancellation + } while (!Socket.Poll (250000, mode)); + + cancellationToken.ThrowIfCancellationRequested (); + } + + protected override void Dispose (bool disposing) + { + if (disposing) { + if (ownsSocket && connected) { + ownsSocket = false; + Disconnect (); + } else { + Cleanup (); + } + } + + base.Dispose (disposing); + } + } +} diff --git a/MailKit/Net/Pop3/AsyncPop3Client.cs b/MailKit/Net/Pop3/AsyncPop3Client.cs new file mode 100644 index 0000000000..63ceb3a740 --- /dev/null +++ b/MailKit/Net/Pop3/AsyncPop3Client.cs @@ -0,0 +1,1787 @@ +// +// AsyncPop3Client.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Net; +using System.Text; +using System.Threading; +using System.Net.Sockets; +using System.Net.Security; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +using MimeKit; + +using MailKit.Security; + +namespace MailKit.Net.Pop3 +{ + public partial class Pop3Client + { + Task SendCommandAsync (CancellationToken token, string command) + { + engine.QueueCommand (null, Encoding.ASCII, command); + + return engine.RunAsync (true, token); + } + + Task SendCommandAsync (CancellationToken token, string format, params object[] args) + { + return SendCommandAsync (token, Encoding.ASCII, format, args); + } + + async Task SendCommandAsync (CancellationToken token, Encoding encoding, string format, params object[] args) + { + var pc = engine.QueueCommand (null, encoding, format, args); + + await engine.RunAsync (true, token).ConfigureAwait (false); + + return pc.StatusText ?? string.Empty; + } + + async Task ProbeCapabilitiesAsync (CancellationToken cancellationToken) + { + if ((engine.Capabilities & Pop3Capabilities.UIDL) == 0 && (probed & ProbedCapabilities.UIDL) == 0) { + // if the message count is > 0, we can probe the UIDL command + if (total > 0) { + try { + await GetMessageUidAsync (0, cancellationToken).ConfigureAwait (false); + } catch (NotSupportedException) { + } + } + } + } + + async Task UpdateMessageCountAsync (CancellationToken cancellationToken) + { + engine.QueueCommand (ProcessStatResponse, "STAT\r\n"); + + await engine.RunAsync (true, cancellationToken).ConfigureAwait (false); + + return Count; + } + + async Task OnAuthenticatedAsync (string message, CancellationToken cancellationToken) + { + engine.State = Pop3EngineState.Transaction; + + await engine.QueryCapabilitiesAsync (cancellationToken).ConfigureAwait (false); + await UpdateMessageCountAsync (cancellationToken).ConfigureAwait (false); + await ProbeCapabilitiesAsync (cancellationToken).ConfigureAwait (false); + OnAuthenticated (message); + } + + /// + /// Asynchronously authenticate using the specified SASL mechanism. + /// + /// + /// Authenticates using the specified SASL mechanism. + /// For a list of available SASL authentication mechanisms supported by the server, + /// check the property after the service has been + /// connected. + /// + /// An asynchronous task context. + /// The SASL mechanism. + /// The cancellation token. + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is already authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Authentication using the supplied credentials has failed. + /// + /// + /// A SASL authentication error occurred. + /// + /// + /// An I/O error occurred. + /// + /// + /// A POP3 command failed. + /// + /// + /// An POP3 protocol error occurred. + /// + public override async Task AuthenticateAsync (SaslMechanism mechanism, CancellationToken cancellationToken = default) + { + var saslUri = CheckCanAuthenticate (mechanism, cancellationToken); + + using var operation = engine.StartNetworkOperation (NetworkOperationKind.Authenticate); + + try { + var ctx = GetSaslAuthContext (mechanism, saslUri); + + var pc = await ctx.AuthenticateAsync (cancellationToken).ConfigureAwait (false); + + if (pc.Status == Pop3CommandStatus.Error) + throw new AuthenticationException (); + + pc.ThrowIfError (); + + await OnAuthenticatedAsync (ctx.AuthMessage!, cancellationToken).ConfigureAwait (false); + } catch (Exception ex) { + operation.SetError (ex); + throw; + } + } + + /// + /// Asynchronously authenticates using the supplied credentials. + /// + /// + /// Asynchronously authenticates using the supplied credentials. + /// If the POP3 server supports the APOP authentication mechanism, + /// then APOP is used. + /// If the APOP authentication mechanism is not supported and the + /// server supports one or more SASL authentication mechanisms, then + /// the SASL mechanisms that both the client and server support (not including + /// any OAUTH mechanisms) are tried in order of greatest security to weakest + /// security. Once a SASL authentication mechanism is found that both client + /// and server support, the credentials are used to authenticate. + /// If the server does not support SASL or if no common SASL mechanisms + /// can be found, then the USER and PASS commands are used as a + /// fallback. + /// To prevent the usage of certain authentication mechanisms, + /// simply remove them from the hash set + /// before calling this method. + /// In the case of the APOP authentication mechanism, remove it from the + /// property instead. + /// + /// An asynchronous task context. + /// The text encoding to use for the user's credentials. + /// The user's credentials. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is already authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Authentication using the supplied credentials has failed. + /// + /// + /// A SASL authentication error occurred. + /// + /// + /// An I/O error occurred. + /// + /// + /// A POP3 command failed. + /// + /// + /// An POP3 protocol error occurred. + /// + public override async Task AuthenticateAsync (Encoding encoding, ICredentials credentials, CancellationToken cancellationToken = default) + { + var saslUri = CheckCanAuthenticate (encoding, credentials, cancellationToken); + + using var operation = engine.StartNetworkOperation (NetworkOperationKind.Authenticate); + + try { + string userName, password; + NetworkCredential? cred; + string? message = null; + + if ((engine.Capabilities & Pop3Capabilities.Apop) != 0 && (cred = credentials.GetCredential (saslUri, "APOP")) != null) { + var apop = GetApopCommand (encoding, cred); + + detector.IsAuthenticating = true; + + try { + message = await SendCommandAsync (cancellationToken, encoding, apop).ConfigureAwait (false); + engine.State = Pop3EngineState.Transaction; + } catch (Pop3CommandException) { + } finally { + detector.IsAuthenticating = false; + } + + if (engine.State == Pop3EngineState.Transaction) { + await OnAuthenticatedAsync (message ?? string.Empty, cancellationToken).ConfigureAwait (false); + return; + } + } + + if ((engine.Capabilities & Pop3Capabilities.Sasl) != 0) { + foreach (var authmech in SaslMechanism.Rank (engine.AuthenticationMechanisms)) { + SaslMechanism? sasl; + + cred = credentials.GetCredential (saslUri, authmech); + + if (cred == null || (sasl = SaslMechanism.Create (authmech, encoding, cred)) == null) + continue; + + cancellationToken.ThrowIfCancellationRequested (); + + var ctx = GetSaslAuthContext (sasl, saslUri); + + var pc = await ctx.AuthenticateAsync (cancellationToken).ConfigureAwait (false); + + if (pc.Status == Pop3CommandStatus.Error) + continue; + + pc.ThrowIfError (); + + await OnAuthenticatedAsync (ctx.AuthMessage!, cancellationToken).ConfigureAwait (false); + return; + } + } + + // fall back to the classic USER & PASS commands... + if ((cred = credentials.GetCredential (saslUri, "DEFAULT")) == null) + throw new AuthenticationException ("No credentials could be found for the POP3 server."); + + userName = utf8 ? SaslMechanism.SaslPrep (cred.UserName) : cred.UserName; + password = utf8 ? SaslMechanism.SaslPrep (cred.Password) : cred.Password; + detector.IsAuthenticating = true; + + try { + await SendCommandAsync (cancellationToken, encoding, "USER {0}\r\n", userName).ConfigureAwait (false); + message = await SendCommandAsync (cancellationToken, encoding, "PASS {0}\r\n", password).ConfigureAwait (false); + } catch (Pop3CommandException) { + throw new AuthenticationException (); + } finally { + detector.IsAuthenticating = false; + } + + await OnAuthenticatedAsync (message, cancellationToken).ConfigureAwait (false); + } catch (Exception ex) { + operation.SetError (ex); + throw; + } + } + + async Task SslHandshakeAsync (SslStream ssl, string host, CancellationToken cancellationToken) + { +#if NET5_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER + await ssl.AuthenticateAsClientAsync (GetSslClientAuthenticationOptions (host, ValidateRemoteCertificate), cancellationToken).ConfigureAwait (false); +#else + await ssl.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, CheckCertificateRevocation).ConfigureAwait (false); +#endif + } + + async Task PostConnectAsync (Stream stream, string host, int port, SecureSocketOptions options, bool starttls, CancellationToken cancellationToken) + { + probed = ProbedCapabilities.None; + + try { + ProtocolLogger.LogConnect (engine.Uri!); + } catch { + stream.Dispose (); + throw; + } + + var pop3 = new Pop3Stream (stream, ProtocolLogger); + + await engine.ConnectAsync (pop3, cancellationToken).ConfigureAwait (false); + + try { + await engine.QueryCapabilitiesAsync (cancellationToken).ConfigureAwait (false); + + if (options == SecureSocketOptions.StartTls && (engine.Capabilities & Pop3Capabilities.StartTLS) == 0) + throw new NotSupportedException ("The POP3 server does not support the STLS extension."); + + if (starttls && (engine.Capabilities & Pop3Capabilities.StartTLS) != 0) { + await SendCommandAsync (cancellationToken, "STLS\r\n").ConfigureAwait (false); + + try { + var tls = new ExtendedSslStream (stream, false, ValidateRemoteCertificate); + pop3.SetStream (tls); + + await SslHandshakeAsync (tls, host, cancellationToken).ConfigureAwait (false); + } catch (Exception ex) { + throw SslHandshakeException.Create (ref sslValidationInfo, ex, true, "POP3", host, port, 995, 110); + } + + engine.IsSecure = true; + + // re-issue a CAPA command + await engine.QueryCapabilitiesAsync (cancellationToken).ConfigureAwait (false); + } + } catch (Exception ex) { + engine.Disconnect (ex); + throw; + } + + engine.Disconnected += OnEngineDisconnected; + OnConnected (host, port, options); + } + + /// + /// Asynchronously establish a connection to the specified POP3 or POP3/S server. + /// + /// + /// Establishes a connection to the specified POP3 or POP3/S server. + /// If the has a value of 0, then the + /// parameter is used to determine the default port to + /// connect to. The default port used with + /// is 995. All other values will use a default port of 110. + /// If the has a value of + /// , then the is used + /// to determine the default security options. If the has a value + /// of 995, then the default options used will be + /// . All other values will use + /// . + /// Once a connection is established, properties such as + /// and will be + /// populated. + /// + /// + /// + /// + /// An asynchronous task context. + /// The host name to connect to. + /// The port to connect to. If the specified port is 0, then the default port will be used. + /// The secure socket options to when connecting. + /// The cancellation token. + /// + /// is . + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The has been disposed. + /// + /// + /// The is already connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// was set to + /// + /// and the POP3 server does not support the STLS extension. + /// + /// + /// A socket error occurred trying to connect to the remote host. + /// + /// + /// An error occurred during the SSL/TLS negotiations. + /// + /// + /// An I/O error occurred. + /// + /// + /// A POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override async Task ConnectAsync (string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default) + { + CheckCanConnect (host, port); + + ComputeDefaultValues (host, ref port, ref options, out var uri, out var starttls); + + using var operation = engine.StartNetworkOperation (NetworkOperationKind.Connect, uri); + + try { + var stream = await ConnectNetworkAsync (host, port, cancellationToken).ConfigureAwait (false); + stream.WriteTimeout = timeout; + stream.ReadTimeout = timeout; + + engine.Uri = uri; + + if (options == SecureSocketOptions.SslOnConnect) { + var ssl = new ExtendedSslStream (stream, false, ValidateRemoteCertificate); + + try { + await SslHandshakeAsync (ssl, host, cancellationToken).ConfigureAwait (false); + } catch (Exception ex) { + ssl.Dispose (); + + throw SslHandshakeException.Create (ref sslValidationInfo, ex, false, "POP3", host, port, 995, 110); + } + + stream = ssl; + } + + await PostConnectAsync (stream, host, port, options, starttls, cancellationToken).ConfigureAwait (false); + } catch (Exception ex) { + operation.SetError (ex); + throw; + } + } + + /// + /// Asynchronously establish a connection to the specified POP3 or POP3/S server using the provided socket. + /// + /// + /// Establishes a connection to the specified POP3 or POP3/S server using + /// the provided socket. + /// If the has a value of + /// , then the is used + /// to determine the default security options. If the has a value + /// of 995, then the default options used will be + /// . All other values will use + /// . + /// Once a connection is established, properties such as + /// and will be + /// populated. + /// With the exception of using the to determine the + /// default to use when the value + /// is , the and + /// parameters are only used for logging purposes. + /// + /// An asynchronous task context. + /// The socket to use for the connection. + /// The host name to connect to. + /// The port to connect to. If the specified port is 0, then the default port will be used. + /// The secure socket options to when connecting. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// is not between 0 and 65535. + /// + /// + /// is not connected. + /// -or- + /// The is a zero-length string. + /// + /// + /// The has been disposed. + /// + /// + /// The is already connected. + /// + /// + /// was set to + /// + /// and the POP3 server does not support the STLS extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An error occurred during the SSL/TLS negotiations. + /// + /// + /// An I/O error occurred. + /// + /// + /// A POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override Task ConnectAsync (Socket socket, string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default) + { + CheckCanConnect (socket, host, port); + + return ConnectAsync (new NetworkStream (socket, true), host, port, options, cancellationToken); + } + + /// + /// Asynchronously establish a connection to the specified POP3 or POP3/S server using the provided stream. + /// + /// + /// Establishes a connection to the specified POP3 or POP3/S server using + /// the provided stream. + /// If the has a value of + /// , then the is used + /// to determine the default security options. If the has a value + /// of 995, then the default options used will be + /// . All other values will use + /// . + /// Once a connection is established, properties such as + /// and will be + /// populated. + /// With the exception of using the to determine the + /// default to use when the value + /// is , the and + /// parameters are only used for logging purposes. + /// + /// An asynchronous task context. + /// The socket to use for the connection. + /// The host name to connect to. + /// The port to connect to. If the specified port is 0, then the default port will be used. + /// The secure socket options to when connecting. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The has been disposed. + /// + /// + /// The is already connected. + /// + /// + /// was set to + /// + /// and the POP3 server does not support the STLS extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An error occurred during the SSL/TLS negotiations. + /// + /// + /// An I/O error occurred. + /// + /// + /// A POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override async Task ConnectAsync (Stream stream, string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default) + { + CheckCanConnect (stream, host, port); + + Stream network; + + ComputeDefaultValues (host, ref port, ref options, out var uri, out var starttls); + + using var operation = engine.StartNetworkOperation (NetworkOperationKind.Connect, uri); + + try { + engine.Uri = uri; + + if (options == SecureSocketOptions.SslOnConnect) { + var ssl = new ExtendedSslStream (stream, false, ValidateRemoteCertificate); + + try { + await SslHandshakeAsync (ssl, host, cancellationToken).ConfigureAwait (false); + } catch (Exception ex) { + ssl.Dispose (); + + throw SslHandshakeException.Create (ref sslValidationInfo, ex, false, "POP3", host, port, 995, 110); + } + + network = ssl; + } else { + network = stream; + } + + if (network.CanTimeout) { + network.WriteTimeout = timeout; + network.ReadTimeout = timeout; + } + + await PostConnectAsync (network, host, port, options, starttls, cancellationToken).ConfigureAwait (false); + } catch (Exception ex) { + operation.SetError (ex); + throw; + } + } + + /// + /// Asynchronously disconnect the service. + /// + /// + /// If is , a QUIT command will be issued in order to disconnect cleanly. + /// + /// + /// + /// + /// An asynchronous task context. + /// If set to , a QUIT command will be issued in order to disconnect cleanly. + /// The cancellation token. + /// + /// The has been disposed. + /// + public override async Task DisconnectAsync (bool quit, CancellationToken cancellationToken = default) + { + CheckDisposed (); + + if (!engine.IsConnected) + return; + + if (quit) { + try { + await SendCommandAsync (cancellationToken, "QUIT\r\n").ConfigureAwait (false); + } catch (OperationCanceledException) { + } catch (Pop3ProtocolException) { + } catch (Pop3CommandException) { + } catch (IOException) { + } + } + + disconnecting = true; + engine.Disconnect (null); + } + + /// + /// Asynchronously get the message count. + /// + /// + /// Asynchronously gets the message count. + /// + /// The message count. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override Task GetMessageCountAsync (CancellationToken cancellationToken = default) + { + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); + + return UpdateMessageCountAsync (cancellationToken); + } + + /// + /// Ping the POP3 server to keep the connection alive. + /// + /// Mail servers, if left idle for too long, will automatically drop the connection. + /// An asynchronous task context. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override Task NoOpAsync (CancellationToken cancellationToken = default) + { + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); + + return SendCommandAsync (cancellationToken, "NOOP\r\n"); + } + + /// + /// Asynchronously enable UTF8 mode. + /// + /// + /// The POP3 UTF8 extension allows the client to retrieve messages in the UTF-8 encoding and + /// may also allow the user to authenticate using a UTF-8 encoded username or password. + /// + /// An asynchronous task context. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The has already been authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// The POP3 server does not support the UTF8 extension. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public async Task EnableUTF8Async (CancellationToken cancellationToken = default) + { + if (!CheckCanEnableUTF8 ()) + return; + + await SendCommandAsync (cancellationToken, "UTF8\r\n").ConfigureAwait (false); + utf8 = true; + } + + static async Task ReadLangResponseAsync (Pop3Engine engine, Pop3Command pc, CancellationToken cancellationToken) + { + var langs = (List) pc.UserData!; + + do { + var response = await engine.ReadLineAsync (cancellationToken).ConfigureAwait (false); + + if (response == ".") + break; + + var tokens = response.Split (Space, 2); + if (tokens.Length != 2) + continue; + + langs.Add (new Pop3Language (tokens[0], tokens[1])); + } while (true); + } + + /// + /// Asynchronously get the list of languages supported by the POP3 server. + /// + /// + /// If the POP3 server supports the LANG extension, it is possible to + /// query the list of languages supported by the POP3 server that can + /// be used for error messages. + /// + /// The supported languages. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// The POP3 server does not support the LANG extension. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public async Task> GetLanguagesAsync (CancellationToken cancellationToken = default) + { + var pc = QueueLangCommand (out var langs); + + await engine.RunAsync (true, cancellationToken).ConfigureAwait (false); + + return new ReadOnlyCollection (langs); + } + + /// + /// Asynchronously set the language used by the POP3 server for error messages. + /// + /// + /// If the POP3 server supports the LANG extension, it is possible to + /// set the language used by the POP3 server for error messages. + /// + /// An asynchronous task context. + /// The language code. + /// The cancellation token. + /// + /// is . + /// + /// + /// is empty. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// The POP3 server does not support the LANG extension. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public Task SetLanguageAsync (string lang, CancellationToken cancellationToken = default) + { + CheckCanSetLanguage (lang); + + return SendCommandAsync (cancellationToken, $"LANG {lang}\r\n"); + } + + /// + /// Asynchronously get the UID of the message at the specified index. + /// + /// + /// Gets the UID of the message at the specified index. + /// Not all servers support UIDs, so you should first check the + /// property for the flag or + /// the convenience property. + /// + /// The message UID. + /// The message index. + /// The cancellation token. + /// + /// is not a valid message index. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The POP3 server does not support the UIDL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override async Task GetMessageUidAsync (int index, CancellationToken cancellationToken = default) + { + var pc = QueueUidlCommand (index); + + await engine.RunAsync (false, cancellationToken).ConfigureAwait (false); + + return OnUidlComplete (pc); + } + + static async Task ReadUidlAllResponseAsync (Pop3Engine engine, Pop3Command pc, CancellationToken cancellationToken) + { + do { + var response = await engine.ReadLineAsync (cancellationToken).ConfigureAwait (false); + + if (response == ".") + break; + + if (pc.Exception != null) + continue; + + ParseUidlAllResponse (pc, response); + } while (true); + } + + /// + /// Asynchronously get the full list of available message UIDs. + /// + /// + /// Gets the full list of available message UIDs. + /// Not all servers support UIDs, so you should first check the + /// property for the flag or + /// the convenience property. + /// + /// + /// + /// + /// The message uids. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The POP3 server does not support the UIDL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override async Task> GetMessageUidsAsync (CancellationToken cancellationToken = default) + { + var pc = QueueUidlCommand (); + + await engine.RunAsync (false, cancellationToken).ConfigureAwait (false); + + return OnUidlComplete> (pc); + } + + /// + /// Asynchronously get the size of the specified message, in bytes. + /// + /// + /// Gets the size of the specified message, in bytes. + /// + /// The message size, in bytes. + /// The index of the message. + /// The cancellation token. + /// + /// is not a valid message index. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override async Task GetMessageSizeAsync (int index, CancellationToken cancellationToken = default) + { + var pc = QueueListCommand (index); + + await engine.RunAsync (true, cancellationToken).ConfigureAwait (false); + + return (int) pc.UserData!; + } + + static async Task ReadListAllResponseAsync (Pop3Engine engine, Pop3Command pc, CancellationToken cancellationToken) + { + do { + var response = await engine.ReadLineAsync (cancellationToken).ConfigureAwait (false); + + if (response == ".") + break; + + if (pc.Exception != null) + continue; + + ParseListAllResponse (pc, response); + } while (true); + } + + /// + /// Asynchronously get the sizes for all available messages, in bytes. + /// + /// + /// Gets the sizes for all available messages, in bytes. + /// + /// The message sizes, in bytes. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override async Task> GetMessageSizesAsync (CancellationToken cancellationToken = default) + { + var sizes = QueueListCommand (); + + await engine.RunAsync (true, cancellationToken).ConfigureAwait (false); + + return sizes; + } + + /// + /// Asynchronously get the headers for the message at the specified index. + /// + /// + /// Gets the headers for the message at the specified index. + /// + /// The message headers. + /// The index of the message. + /// The cancellation token. + /// + /// is not a valid message index. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override Task GetMessageHeadersAsync (int index, CancellationToken cancellationToken = default) + { + CheckCanDownload (index); + + var ctx = new DownloadHeaderContext (this, parser); + + return ctx.DownloadAsync (index, true, cancellationToken); + } + + /// + /// Asynchronously get the headers for the messages at the specified indexes. + /// + /// + /// Gets the headers for the messages at the specified indexes. + /// When the POP3 server supports the + /// extension, this method will likely be more efficient than using + /// for each message because + /// it will batch the commands to reduce latency. + /// + /// The headers for the specified messages. + /// The indexes of the messages. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the are invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The POP3 server does not support the UIDL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override Task> GetMessageHeadersAsync (IList indexes, CancellationToken cancellationToken = default) + { + if (!CheckCanDownload (indexes)) + return Task.FromResult ((IList) Array.Empty ()); + + var ctx = new DownloadHeaderContext (this, parser); + + return ctx.DownloadAsync (indexes, true, cancellationToken); + } + + /// + /// Asynchronously get the headers of the messages within the specified range. + /// + /// + /// Gets the headers of the messages within the specified range. + /// When the POP3 server supports the + /// extension, this method will likely be more efficient than using + /// for each message because + /// it will batch the commands to reduce latency. + /// + /// The headers of the messages within the specified range. + /// The index of the first message to get. + /// The number of messages to get. + /// The cancellation token. + /// + /// and do not specify + /// a valid range of messages. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The POP3 server does not support the UIDL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override Task> GetMessageHeadersAsync (int startIndex, int count, CancellationToken cancellationToken = default) + { + if (!CheckCanDownload (startIndex, count)) + return Task.FromResult ((IList) Array.Empty ()); + + var ctx = new DownloadHeaderContext (this, parser); + + return ctx.DownloadAsync (startIndex, count, true, cancellationToken); + } + + /// + /// Asynchronously get the message at the specified index. + /// + /// + /// Gets the message at the specified index. + /// + /// + /// + /// + /// The message. + /// The index of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is not a valid message index. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override Task GetMessageAsync (int index, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + CheckCanDownload (index); + + var ctx = new DownloadMessageContext (this, parser, progress); + + return ctx.DownloadAsync (index, false, cancellationToken); + } + + /// + /// Asynchronously get the messages at the specified indexes. + /// + /// + /// Gets the messages at the specified indexes. + /// When the POP3 server supports the + /// extension, this method will likely be more efficient than using + /// for each message + /// because it will batch the commands to reduce latency. + /// + /// The messages. + /// The indexes of the messages. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// + /// + /// One or more of the are invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The POP3 server does not support the UIDL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override Task> GetMessagesAsync (IList indexes, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + if (!CheckCanDownload (indexes)) + return Task.FromResult ((IList) Array.Empty ()); + + var ctx = new DownloadMessageContext (this, parser, progress); + + return ctx.DownloadAsync (indexes, false, cancellationToken); + } + + /// + /// Asynchronously get the messages within the specified range. + /// + /// + /// Gets the messages within the specified range. + /// When the POP3 server supports the + /// extension, this method will likely be more efficient than using + /// for each message + /// because it will batch the commands to reduce latency. + /// + /// + /// + /// + /// The messages. + /// The index of the first message to get. + /// The number of messages to get. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// and do not specify + /// a valid range of messages. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The POP3 server does not support the UIDL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override Task> GetMessagesAsync (int startIndex, int count, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + if (!CheckCanDownload (startIndex, count)) + return Task.FromResult ((IList) Array.Empty ()); + + var ctx = new DownloadMessageContext (this, parser, progress); + + return ctx.DownloadAsync (startIndex, count, false, cancellationToken); + } + + /// + /// Asynchronously get the message or header stream at the specified index. + /// + /// + /// Gets the message or header stream at the specified index. + /// + /// The message or header stream. + /// The index of the message. + /// if only the headers should be retrieved; otherwise, . + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is not a valid message index. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override Task GetStreamAsync (int index, bool headersOnly = false, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + CheckCanDownload (index); + + var ctx = new DownloadStreamContext (this, progress); + + return ctx.DownloadAsync (index, headersOnly, cancellationToken); + } + + /// + /// Asynchronously get the message or header streams at the specified indexes. + /// + /// + /// Get the message or header streams at the specified indexes. + /// If the POP3 server supports the + /// extension, this method will likely be more efficient than using + /// for each message + /// because it will batch the commands to reduce latency. + /// + /// The message or header streams. + /// The indexes of the messages. + /// if only the headers should be retrieved; otherwise, . + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// + /// + /// One or more of the are invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The POP3 server does not support the UIDL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override Task> GetStreamsAsync (IList indexes, bool headersOnly = false, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + if (!CheckCanDownload (indexes)) + return Task.FromResult ((IList) Array.Empty ()); + + var ctx = new DownloadStreamContext (this, progress); + + return ctx.DownloadAsync (indexes, headersOnly, cancellationToken); + } + + /// + /// Asynchronously get the message or header streams within the specified range. + /// + /// + /// Gets the message or header streams within the specified range. + /// If the POP3 server supports the + /// extension, this method will likely be more efficient than using + /// for each message + /// because it will batch the commands to reduce latency. + /// + /// The message or header streams. + /// The index of the first stream to get. + /// The number of streams to get. + /// if only the headers should be retrieved; otherwise, . + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// and do not specify + /// a valid range of messages. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The POP3 server does not support the UIDL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override Task> GetStreamsAsync (int startIndex, int count, bool headersOnly = false, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + if (!CheckCanDownload (startIndex, count)) + return Task.FromResult ((IList) Array.Empty ()); + + var ctx = new DownloadStreamContext (this, progress); + + return ctx.DownloadAsync (startIndex, count, headersOnly, cancellationToken); + } + + /// + /// Asynchronously mark the specified message for deletion. + /// + /// + /// Messages marked for deletion are not actually deleted until the session + /// is cleanly disconnected + /// (see ). + /// + /// + /// + /// + /// An asynchronous task context. + /// The index of the message. + /// The cancellation token. + /// + /// is not a valid message index. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override Task DeleteMessageAsync (int index, CancellationToken cancellationToken = default) + { + CheckCanDelete (index, out string seqid); + + return SendCommandAsync (cancellationToken, $"DELE {seqid}\r\n"); + } + + /// + /// Asynchronously mark the specified messages for deletion. + /// + /// + /// Messages marked for deletion are not actually deleted until the session + /// is cleanly disconnected + /// (see ). + /// + /// An asynchronous task context. + /// The indexes of the messages. + /// The cancellation token. + /// + /// is . + /// + /// + /// One or more of the are invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override async Task DeleteMessagesAsync (IList indexes, CancellationToken cancellationToken = default) + { + if (!CheckCanDelete (indexes)) + return; + + if ((Capabilities & Pop3Capabilities.Pipelining) == 0) { + for (int i = 0; i < indexes.Count; i++) + await SendCommandAsync (cancellationToken, "DELE {0}\r\n", indexes[i] + 1).ConfigureAwait (false); + + return; + } + + for (int i = 0; i < indexes.Count; i++) + engine.QueueCommand (null, "DELE {0}\r\n", indexes[i] + 1); + + await engine.RunAsync (true, cancellationToken).ConfigureAwait (false); + } + + /// + /// Asynchronously mark the specified range of messages for deletion. + /// + /// + /// Messages marked for deletion are not actually deleted until the session + /// is cleanly disconnected + /// (see ). + /// + /// + /// + /// + /// An asynchronous task context. + /// The index of the first message to mark for deletion. + /// The number of messages to mark for deletion. + /// The cancellation token. + /// + /// and do not specify + /// a valid range of messages. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override async Task DeleteMessagesAsync (int startIndex, int count, CancellationToken cancellationToken = default) + { + if (!CheckCanDelete (startIndex, count)) + return; + + if ((Capabilities & Pop3Capabilities.Pipelining) == 0) { + for (int i = 0; i < count; i++) + await SendCommandAsync (cancellationToken, "DELE {0}\r\n", startIndex + i + 1).ConfigureAwait (false); + + return; + } + + for (int i = 0; i < count; i++) + engine.QueueCommand (null, "DELE {0}\r\n", startIndex + i + 1); + + await engine.RunAsync (true, cancellationToken).ConfigureAwait (false); + } + + /// + /// Asynchronously mark all messages for deletion. + /// + /// + /// Messages marked for deletion are not actually deleted until the session + /// is cleanly disconnected + /// (see ). + /// + /// An asynchronous task context. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override Task DeleteAllMessagesAsync (CancellationToken cancellationToken = default) + { + if (total > 0) + return DeleteMessagesAsync (0, total, cancellationToken); + + return Task.CompletedTask; + } + + /// + /// Asynchronously reset the state of all messages marked for deletion. + /// + /// + /// Messages marked for deletion are not actually deleted until the session + /// is cleanly disconnected + /// (see ). + /// + /// An awaitable task. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override Task ResetAsync (CancellationToken cancellationToken = default) + { + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); + + return SendCommandAsync (cancellationToken, "RSET\r\n"); + } + } +} diff --git a/MailKit/Net/Pop3/IPop3Client.cs b/MailKit/Net/Pop3/IPop3Client.cs new file mode 100644 index 0000000000..199d0aca25 --- /dev/null +++ b/MailKit/Net/Pop3/IPop3Client.cs @@ -0,0 +1,309 @@ +// +// IPop3Client.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; + +namespace MailKit.Net.Pop3 { + /// + /// An interface for a POP3 client. + /// + /// + /// Implemented by . + /// + public interface IPop3Client : IMailSpool + { + /// + /// Gets the capabilities supported by the POP3 server. + /// + /// + /// The capabilities will not be known until a successful connection has been made + /// and may change once the client is authenticated. + /// + /// + /// + /// + /// The capabilities. + /// + /// Capabilities cannot be enabled, they may only be disabled. + /// + Pop3Capabilities Capabilities { get; set; } + + /// + /// Gets the expiration policy. + /// + /// + /// If the server supports the EXPIRE capability (), the value + /// of the property will reflect the value advertized by the server. + /// A value of -1 indicates that messages will never expire. + /// A value of 0 indicates that messages that have been retrieved during the current session + /// will be purged immediately after the connection is closed via the QUIT command. + /// Values larger than 0 indicate the minimum number of days that the server will retain + /// messages which have been retrieved. + /// + /// + /// + /// + /// The expiration policy. + int ExpirePolicy { get; } + + /// + /// Gets the implementation details of the server. + /// + /// + /// If the server advertizes its implementation details, this value will be set to a string containing the + /// information details provided by the server. + /// + /// The implementation details. + string? Implementation { get; } + + /// + /// Gets the minimum delay, in milliseconds, between logins. + /// + /// + /// If the server supports the LOGIN-DELAY capability (), this value + /// will be set to the minimum number of milliseconds that the client must wait between logins. + /// + /// + /// + /// + /// The login delay. + int LoginDelay { get; } + + /// + /// Enable UTF8 mode. + /// + /// + /// The POP3 UTF8 extension allows the client to retrieve messages in the UTF-8 encoding and + /// may also allow the user to authenticate using a UTF-8 encoded username or password. + /// + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The has already been authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// The POP3 server does not support the UTF8 extension. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + void EnableUTF8 (CancellationToken cancellationToken = default); + + /// + /// Asynchronously enable UTF8 mode. + /// + /// + /// The POP3 UTF8 extension allows the client to retrieve messages in the UTF-8 encoding and + /// may also allow the user to authenticate using a UTF-8 encoded username or password. + /// + /// An asynchronous task context. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The has already been authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// The POP3 server does not support the UTF8 extension. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + Task EnableUTF8Async (CancellationToken cancellationToken = default); + + /// + /// Get the list of languages supported by the POP3 server. + /// + /// + /// If the POP3 server supports the LANG extension, it is possible to + /// query the list of languages supported by the POP3 server that can + /// be used for error messages. + /// + /// The supported languages. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// The POP3 server does not support the LANG extension. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + IList GetLanguages (CancellationToken cancellationToken = default); + + /// + /// Asynchronously get the list of languages supported by the POP3 server. + /// + /// + /// If the POP3 server supports the LANG extension, it is possible to + /// query the list of languages supported by the POP3 server that can + /// be used for error messages. + /// + /// The supported languages. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// The POP3 server does not support the LANG extension. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + Task> GetLanguagesAsync (CancellationToken cancellationToken = default); + + /// + /// Set the language used by the POP3 server for error messages. + /// + /// + /// If the POP3 server supports the LANG extension, it is possible to + /// set the language used by the POP3 server for error messages. + /// + /// The language code. + /// The cancellation token. + /// + /// is . + /// + /// + /// is empty. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// The POP3 server does not support the LANG extension. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + void SetLanguage (string lang, CancellationToken cancellationToken = default); + + /// + /// Asynchronously set the language used by the POP3 server for error messages. + /// + /// + /// If the POP3 server supports the LANG extension, it is possible to + /// set the language used by the POP3 server for error messages. + /// + /// An asynchronous task context. + /// The language code. + /// The cancellation token. + /// + /// is . + /// + /// + /// is empty. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// The POP3 server does not support the LANG extension. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + Task SetLanguageAsync (string lang, CancellationToken cancellationToken = default); + } +} diff --git a/MailKit/Net/Pop3/Pop3AuthenticationSecretDetector.cs b/MailKit/Net/Pop3/Pop3AuthenticationSecretDetector.cs new file mode 100644 index 0000000000..9447cebfe3 --- /dev/null +++ b/MailKit/Net/Pop3/Pop3AuthenticationSecretDetector.cs @@ -0,0 +1,343 @@ +// +// Pop3AuthenticationSecretDetector.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Collections.Generic; + +namespace MailKit.Net.Pop3 { + class Pop3AuthenticationSecretDetector : IAuthenticationSecretDetector + { + static readonly IList EmptyAuthSecrets = Array.Empty (); + + enum Pop3AuthCommandState + { + None, + A, + Apop, + ApopUserName, + ApopToken, + ApopNewLine, + Auth, + AuthMechanism, + AuthNewLine, + AuthToken, + User, + UserName, + UserNewLine, + Pass, + Password, + PassNewLine, + Error + } + + Pop3AuthCommandState state; + bool isAuthenticating; + int commandIndex; + + public bool IsAuthenticating { + get { return isAuthenticating; } + set { + state = Pop3AuthCommandState.None; + isAuthenticating = value; + commandIndex = 0; + } + } + + bool SkipCommand (string command, byte[] buffer, ref int index, int endIndex) + { + while (index < endIndex && commandIndex < command.Length) { + if (buffer[index] != (byte) command[commandIndex]) { + state = Pop3AuthCommandState.Error; + break; + } + + commandIndex++; + index++; + } + + return commandIndex == command.Length; + } + + IList DetectApopSecrets (byte[] buffer, int offset, int endIndex) + { + var secrets = new List (); + int index = offset; + int startIndex; + + if (state == Pop3AuthCommandState.ApopNewLine) + return EmptyAuthSecrets; + + if (state == Pop3AuthCommandState.Apop) { + if (SkipCommand ("APOP ", buffer, ref index, endIndex)) + state = Pop3AuthCommandState.ApopUserName; + + if (index >= endIndex || state == Pop3AuthCommandState.Error) + return EmptyAuthSecrets; + } + + if (state == Pop3AuthCommandState.ApopUserName) { + startIndex = index; + + while (index < endIndex && buffer[index] != (byte) ' ') + index++; + + if (index > startIndex) + secrets.Add (new AuthenticationSecret (startIndex, index - startIndex)); + + if (index < endIndex) { + state = Pop3AuthCommandState.ApopToken; + index++; + } + + if (index >= endIndex) + return secrets; + } + + startIndex = index; + + while (index < endIndex && buffer[index] != (byte) '\r') + index++; + + if (index < endIndex) + state = Pop3AuthCommandState.ApopNewLine; + + if (index > startIndex) + secrets.Add (new AuthenticationSecret (startIndex, index - startIndex)); + + return secrets; + } + + IList DetectAuthSecrets (byte[] buffer, int offset, int endIndex) + { + int index = offset; + + if (state == Pop3AuthCommandState.Auth) { + if (SkipCommand ("AUTH ", buffer, ref index, endIndex)) + state = Pop3AuthCommandState.AuthMechanism; + + if (index >= endIndex || state == Pop3AuthCommandState.Error) + return EmptyAuthSecrets; + } + + if (state == Pop3AuthCommandState.AuthMechanism) { + while (index < endIndex && buffer[index] != (byte) ' ' && buffer[index] != (byte) '\r') + index++; + + if (index < endIndex) { + if (buffer[index] == (byte) ' ') { + state = Pop3AuthCommandState.AuthToken; + } else { + state = Pop3AuthCommandState.AuthNewLine; + } + + index++; + } + + if (index >= endIndex) + return EmptyAuthSecrets; + } + + if (state == Pop3AuthCommandState.AuthNewLine) { + if (buffer[index] == (byte) '\n') { + state = Pop3AuthCommandState.AuthToken; + index++; + } else { + state = Pop3AuthCommandState.Error; + } + + if (index >= endIndex || state == Pop3AuthCommandState.Error) + return EmptyAuthSecrets; + } + + int startIndex = index; + while (index < endIndex && buffer[index] != (byte) '\r') + index++; + + if (index < endIndex) + state = Pop3AuthCommandState.AuthNewLine; + + if (index == startIndex) + return EmptyAuthSecrets; + + var secret = new AuthenticationSecret (startIndex, index - startIndex); + + if (state == Pop3AuthCommandState.AuthNewLine) { + index++; + + if (index < endIndex) { + if (buffer[index] == (byte) '\n') { + state = Pop3AuthCommandState.AuthToken; + } else { + state = Pop3AuthCommandState.Error; + } + } + } + + return new AuthenticationSecret[] { secret }; + } + + IList DetectUserPassSecrets (byte[] buffer, int offset, int endIndex) + { + var secrets = new List (); + int index = offset; + + if (state == Pop3AuthCommandState.User) { + if (SkipCommand ("USER ", buffer, ref index, endIndex)) + state = Pop3AuthCommandState.UserName; + + if (index >= endIndex || state == Pop3AuthCommandState.Error) + return EmptyAuthSecrets; + } + + if (state == Pop3AuthCommandState.UserName) { + int startIndex = index; + + while (index < endIndex && buffer[index] != (byte) '\r') + index++; + + if (index > startIndex) + secrets.Add (new AuthenticationSecret (startIndex, index - startIndex)); + + if (index < endIndex) { + state = Pop3AuthCommandState.UserNewLine; + index++; + } + + if (index >= endIndex) + return secrets; + } + + if (state == Pop3AuthCommandState.UserNewLine) { + if (buffer[index] == (byte) '\n') { + state = Pop3AuthCommandState.Pass; + commandIndex = 0; + index++; + } else { + state = Pop3AuthCommandState.Error; + } + + if (index >= endIndex || state == Pop3AuthCommandState.Error) + return secrets; + } + + if (state == Pop3AuthCommandState.Pass) { + if (SkipCommand ("PASS ", buffer, ref index, endIndex)) + state = Pop3AuthCommandState.Password; + + if (index >= endIndex || state == Pop3AuthCommandState.Error) + return EmptyAuthSecrets; + } + + if (state == Pop3AuthCommandState.Password) { + int startIndex = index; + + while (index < endIndex && buffer[index] != (byte) '\r') + index++; + + if (index > startIndex) + secrets.Add (new AuthenticationSecret (startIndex, index - startIndex)); + + if (index < endIndex) { + state = Pop3AuthCommandState.PassNewLine; + index++; + } + + if (index >= endIndex) + return secrets; + } + + if (state == Pop3AuthCommandState.PassNewLine) { + if (buffer[index] == (byte) '\n') { + state = Pop3AuthCommandState.None; + commandIndex = 0; + index++; + } else { + state = Pop3AuthCommandState.Error; + } + } + + return secrets; + } + + public IList DetectSecrets (byte[] buffer, int offset, int count) + { + if (!IsAuthenticating || state == Pop3AuthCommandState.Error || count == 0) + return EmptyAuthSecrets; + + int endIndex = offset + count; + int index = offset; + + if (state == Pop3AuthCommandState.None) { + switch ((char) buffer[index]) { + case 'A': + state = Pop3AuthCommandState.A; + index++; + break; + case 'U': + state = Pop3AuthCommandState.User; + commandIndex = 1; + index++; + break; + default: + state = Pop3AuthCommandState.Error; + break; + } + + if (index >= endIndex || state == Pop3AuthCommandState.Error) + return EmptyAuthSecrets; + } + + if (state == Pop3AuthCommandState.A) { + switch ((char) buffer[index]) { + case 'P': + state = Pop3AuthCommandState.Apop; + commandIndex = 2; + index++; + break; + case 'U': + state = Pop3AuthCommandState.Auth; + commandIndex = 2; + index++; + break; + default: + state = Pop3AuthCommandState.Error; + break; + } + + if (index >= endIndex || state == Pop3AuthCommandState.Error) + return EmptyAuthSecrets; + } + + if (state >= Pop3AuthCommandState.Apop && state <= Pop3AuthCommandState.ApopNewLine) + return DetectApopSecrets (buffer, index, endIndex); + + if (state >= Pop3AuthCommandState.Auth && state <= Pop3AuthCommandState.AuthToken) + return DetectAuthSecrets (buffer, index, endIndex); + + return DetectUserPassSecrets (buffer, index, endIndex); + } + } +} diff --git a/MailKit/Net/Pop3/Pop3Capabilities.cs b/MailKit/Net/Pop3/Pop3Capabilities.cs index 34df359b7a..37f4d250b1 100644 --- a/MailKit/Net/Pop3/Pop3Capabilities.cs +++ b/MailKit/Net/Pop3/Pop3Capabilities.cs @@ -1,9 +1,9 @@ -// +// // Pop3Capabilities.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -39,7 +39,7 @@ namespace MailKit.Net.Pop3 { /// /// [Flags] - public enum Pop3Capabilities { + public enum Pop3Capabilities : uint { /// /// The server does not support any additional extensions. /// @@ -49,81 +49,81 @@ public enum Pop3Capabilities { /// The server supports APOP /// authentication. /// - Apop = (1 << 0), + Apop = 1 << 0, /// /// The server supports the EXPIRE extension /// and defines the expiration policy for messages (see ). /// - Expire = (1 << 1), + Expire = 1 << 1, /// /// The server supports the LOGIN-DELAY extension, /// allowing the server to specify to the client a minimum number of seconds between login attempts /// (see ). /// - LoginDelay = (1 << 2), + LoginDelay = 1 << 2, /// /// The server supports the PIPELINING extension, /// allowing the client to batch multiple requests to the server at at time. /// - Pipelining = (1 << 3), + Pipelining = 1 << 3, /// /// The server supports the RESP-CODES extension, /// allowing the server to provide clients with extended information in error responses. /// - ResponseCodes = (1 << 4), + ResponseCodes = 1 << 4, /// /// The server supports the SASL authentication /// extension, allowing the client to authenticate using the advertized authentication mechanisms /// (see ). /// - Sasl = (1 << 5), + Sasl = 1 << 5, /// /// The server supports the STLS extension, /// allowing clients to switch to an encrypted SSL/TLS connection after connecting. /// - StartTLS = (1 << 6), + StartTLS = 1 << 6, /// /// The server supports the TOP command, /// allowing clients to fetch the headers plus an arbitrary number of lines. /// - Top = (1 << 7), + Top = 1 << 7, /// /// The server supports the UIDL command, /// allowing the client to refer to messages via a UID as opposed to a sequence ID. /// - UIDL = (1 << 8), + UIDL = 1 << 8, /// /// The server supports the USER /// authentication command, allowing the client to authenticate via a plain-text username /// and password command (not recommended unless no other authentication mechanisms exist). /// - User = (1 << 9), + User = 1 << 9, /// /// The server supports the UTF8 extension, /// allowing clients to retrieve messages in the UTF-8 encoding. /// - UTF8 = (1 << 10), + UTF8 = 1 << 10, /// /// The server supports the UTF8=USER extension, /// allowing clients to authenticate using UTF-8 encoded usernames and passwords. /// - UTF8User = (1 << 11), + UTF8User = 1 << 11, /// /// The server supports the LANG extension, /// allowing clients to specify which language the server should use for error strings. /// - Lang = (1 << 12), + Lang = 1 << 12, } } diff --git a/MailKit/Net/Pop3/Pop3Client.cs b/MailKit/Net/Pop3/Pop3Client.cs index b6e006c0e8..1e1d1500d5 100644 --- a/MailKit/Net/Pop3/Pop3Client.cs +++ b/MailKit/Net/Pop3/Pop3Client.cs @@ -1,9 +1,9 @@ -// +// // Pop3Client.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -27,31 +27,26 @@ using System; using System.IO; using System.Net; -using System.Linq; using System.Text; +using System.Buffers; using System.Threading; +using System.Net.Sockets; +using System.Net.Security; +using System.Globalization; using System.Threading.Tasks; using System.Collections.Generic; using System.Collections.ObjectModel; - -#if NETFX_CORE -using Windows.Networking; -using Windows.Networking.Sockets; -using Windows.Storage.Streams; -using Encoding = Portable.Text.Encoding; -using MD5 = MimeKit.Cryptography.MD5; -#else -using System.Net.Sockets; -using System.Net.Security; using System.Security.Cryptography; +using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; -#endif using MimeKit; using MimeKit.IO; using MailKit.Security; +using AuthenticationException = MailKit.Security.AuthenticationException; + namespace MailKit.Net.Pop3 { /// /// A POP3 client that can be used to retrieve messages from a server. @@ -65,25 +60,25 @@ namespace MailKit.Net.Pop3 { /// /// /// - public class Pop3Client : MailSpool + public partial class Pop3Client : MailSpool, IPop3Client { [Flags] enum ProbedCapabilities : byte { None = 0, Top = (1 << 0), - UIDL = (1 << 1), - User = (1 << 2), + UIDL = (1 << 1) } - readonly Dictionary dict = new Dictionary (); + static readonly char[] Space = new char[] { ' ' }; + + readonly Pop3AuthenticationSecretDetector detector = new Pop3AuthenticationSecretDetector (); readonly MimeParser parser = new MimeParser (Stream.Null); readonly Pop3Engine engine; + SslCertificateValidationInfo? sslValidationInfo; ProbedCapabilities probed; -#if NETFX_CORE - StreamSocket socket; -#endif - bool disposed, secure, utf8; - int timeout = 100000; + bool disposed, disconnecting, utf8; + int timeout = 2 * 60 * 1000; + long octets; int total; /// @@ -100,10 +95,11 @@ enum ProbedCapabilities : byte { /// /// The protocol logger. /// - /// is null. + /// is . /// public Pop3Client (IProtocolLogger protocolLogger) : base (protocolLogger) { + protocolLogger.AuthenticationSecretDetector = detector; engine = new Pop3Engine (); } @@ -196,7 +192,7 @@ public int ExpirePolicy { /// information details provided by the server. /// /// The implementation details. - public string Implementation { + public string? Implementation { get { return engine.Implementation; } } @@ -215,6 +211,33 @@ public int LoginDelay { get { return engine.LoginDelay; } } + /// + /// Get the size of the POP3 mailbox, in bytes. + /// + /// + /// Gets the size of the POP3 mailbox, in bytes. + /// This value is updated as a side-effect of calling or . + /// + /// The size of the mailbox if available. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + public long Size { + get { + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); + + return octets; + } + } + void CheckDisposed () { if (disposed) @@ -233,52 +256,61 @@ void CheckAuthenticated () throw new ServiceNotAuthenticatedException ("The Pop3Client has not been authenticated."); } -#if !NETFX_CORE - bool ValidateRemoteCertificate (object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) + bool ValidateRemoteCertificate (object? sender, X509Certificate? certificate, X509Chain? chain, SslPolicyErrors sslPolicyErrors) { - if (ServerCertificateValidationCallback != null) - return ServerCertificateValidationCallback (engine.Uri.Host, certificate, chain, sslPolicyErrors); + var host = engine.Uri!.Host; + bool valid; -#if !NETSTANDARD - if (ServicePointManager.ServerCertificateValidationCallback != null) - return ServicePointManager.ServerCertificateValidationCallback (engine.Uri.Host, certificate, chain, sslPolicyErrors); -#endif + sslValidationInfo?.Dispose (); + sslValidationInfo = null; - return DefaultServerCertificateValidationCallback (sender, certificate, chain, sslPolicyErrors); - } + if (ServerCertificateValidationCallback != null) { + valid = ServerCertificateValidationCallback (host, certificate, chain, sslPolicyErrors); +#if NETFRAMEWORK + } else if (ServicePointManager.ServerCertificateValidationCallback != null) { + valid = ServicePointManager.ServerCertificateValidationCallback (host, certificate, chain, sslPolicyErrors); #endif + } else { + valid = DefaultServerCertificateValidationCallback (host, certificate, chain, sslPolicyErrors); + } - static Exception CreatePop3Exception (Pop3Command pc) - { - var command = pc.Command.Split (' ')[0].TrimEnd (); - var message = string.Format ("POP3 server did not respond with a +OK response to the {0} command.", command); - - if (pc.Status == Pop3CommandStatus.Error) - return new Pop3CommandException (message, pc.StatusText); + if (!valid) { + // Note: The SslHandshakeException.Create() method will nullify this once it's done using it. + sslValidationInfo = new SslCertificateValidationInfo (host, certificate, chain, sslPolicyErrors); + } - return new Pop3ProtocolException (message); + return valid; } static ProtocolException CreatePop3ParseException (Exception innerException, string format, params object[] args) { - return new Pop3ProtocolException (string.Format (format, args), innerException); + return new Pop3ProtocolException (string.Format (CultureInfo.InvariantCulture, format, args), innerException); } static ProtocolException CreatePop3ParseException (string format, params object[] args) { - return new Pop3ProtocolException (string.Format (format, args)); + return new Pop3ProtocolException (string.Format (CultureInfo.InvariantCulture, format, args)); } - void SendCommand (CancellationToken token, string command) + static int GetExpectedSequenceId (Pop3Command pc) { - var pc = engine.QueueCommand (token, null, Encoding.ASCII, command); + int index = pc.Command.IndexOf (' ') + 1; + int endIndex = pc.Command.IndexOf ('\r', index); - while (engine.Iterate () < pc.Id) { - // continue processing commands - } +#if NET5_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER + var seqid = pc.Command.AsSpan (index, endIndex - index); +#else + var seqid = pc.Command.Substring (index, endIndex - index); +#endif - if (pc.Status != Pop3CommandStatus.Ok) - throw CreatePop3Exception (pc); + return int.Parse (seqid, NumberStyles.None, CultureInfo.InvariantCulture); + } + + void SendCommand (CancellationToken token, string command) + { + engine.QueueCommand (null, Encoding.ASCII, command); + + engine.Run (true, token); } string SendCommand (CancellationToken token, string format, params object[] args) @@ -288,29 +320,11 @@ string SendCommand (CancellationToken token, string format, params object[] args string SendCommand (CancellationToken token, Encoding encoding, string format, params object[] args) { - string okText = string.Empty; - - var pc = engine.QueueCommand (token, (pop3, cmd, text) => { - if (cmd.Status == Pop3CommandStatus.Ok) - okText = text; - }, encoding, format, args); - - while (engine.Iterate () < pc.Id) { - // continue processing commands - } - - if (pc.Status != Pop3CommandStatus.Ok) - throw CreatePop3Exception (pc); - - return okText; - } + var pc = engine.QueueCommand (null, encoding, format, args); - void LoadUids () - { - if (dict.Count > 0) - return; + engine.Run (true, token); - GetMessageUids (); + return pc.StatusText ?? string.Empty; } #region IMailService implementation @@ -351,7 +365,7 @@ public override HashSet AuthenticationMechanisms { public override int Timeout { get { return timeout; } set { - if (IsConnected && engine.Stream.CanTimeout) { + if (engine.IsConnected && engine.Stream.CanTimeout) { engine.Stream.WriteTimeout = value; engine.Stream.ReadTimeout = value; } @@ -364,9 +378,9 @@ public override int Timeout { /// Gets whether or not the client is currently connected to an POP3 server. /// /// - /// The state is set to true immediately after + /// The state is set to immediately after /// one of the Connect - /// methods succeeds and is not set back to false until either the client + /// methods succeeds and is not set back to until either the client /// is disconnected via or until a /// is thrown while attempting to read or write to /// the underlying network socket. @@ -376,7 +390,7 @@ public override int Timeout { /// /// /// - /// true if the client is connected; otherwise, false. + /// if the client is connected; otherwise, . public override bool IsConnected { get { return engine.IsConnected; } } @@ -387,9 +401,200 @@ public override bool IsConnected { /// /// Gets whether or not the connection is secure (typically via SSL or TLS). /// - /// true if the connection is secure; otherwise, false. + /// if the connection is secure; otherwise, . public override bool IsSecure { - get { return IsConnected && secure; } + get { return engine.IsSecure; } + } + + /// + /// Get whether or not the connection is encrypted (typically via SSL or TLS). + /// + /// + /// Gets whether or not the connection is encrypted (typically via SSL or TLS). + /// + /// if the connection is encrypted; otherwise, . + public override bool IsEncrypted { + get { return engine.IsSecure && (engine.Stream.Stream is SslStream sslStream) && sslStream.IsEncrypted; } + } + + /// + /// Get whether or not the connection is signed (typically via SSL or TLS). + /// + /// + /// Gets whether or not the connection is signed (typically via SSL or TLS). + /// + /// if the connection is signed; otherwise, . + public override bool IsSigned { + get { return engine.IsSecure && (engine.Stream.Stream is SslStream sslStream) && sslStream.IsSigned; } + } + + /// + /// Get the negotiated SSL or TLS protocol version. + /// + /// + /// Gets the negotiated SSL or TLS protocol version once an SSL or TLS connection has been made. + /// + /// + /// + /// + /// The negotiated SSL or TLS protocol version. + public override SslProtocols SslProtocol { + get { + if (engine.IsSecure && (engine.Stream.Stream is SslStream sslStream)) + return sslStream.SslProtocol; + + return SslProtocols.None; + } + } + + /// + /// Get the negotiated SSL or TLS cipher algorithm. + /// + /// + /// Gets the negotiated SSL or TLS cipher algorithm once an SSL or TLS connection has been made. + /// + /// + /// + /// + /// The negotiated SSL or TLS cipher algorithm. +#if NET10_0_OR_GREATER + [Obsolete ("Use SslCipherSuite instead.")] +#endif + public override CipherAlgorithmType? SslCipherAlgorithm { + get { + if (engine.IsSecure && (engine.Stream.Stream is SslStream sslStream)) + return sslStream.CipherAlgorithm; + + return null; + } + } + + /// + /// Get the negotiated SSL or TLS cipher algorithm strength. + /// + /// + /// Gets the negotiated SSL or TLS cipher algorithm strength once an SSL or TLS connection has been made. + /// + /// + /// + /// + /// The negotiated SSL or TLS cipher algorithm strength. +#if NET10_0_OR_GREATER + [Obsolete ("Use SslCipherSuite instead.")] +#endif + public override int? SslCipherStrength { + get { + if (engine.IsSecure && (engine.Stream.Stream is SslStream sslStream)) + return sslStream.CipherStrength; + + return null; + } + } + +#if NET5_0_OR_GREATER + /// + /// Get the negotiated SSL or TLS cipher suite. + /// + /// + /// Gets the negotiated SSL or TLS cipher suite once an SSL or TLS connection has been made. + /// + /// The negotiated SSL or TLS cipher suite. + public override TlsCipherSuite? SslCipherSuite { + get { + if (engine.IsSecure && (engine.Stream.Stream is SslStream sslStream)) + return sslStream.NegotiatedCipherSuite; + + return null; + } + } +#endif + + /// + /// Get the negotiated SSL or TLS hash algorithm. + /// + /// + /// Gets the negotiated SSL or TLS hash algorithm once an SSL or TLS connection has been made. + /// + /// + /// + /// + /// The negotiated SSL or TLS hash algorithm. +#if NET10_0_OR_GREATER + [Obsolete ("Use SslCipherSuite instead.")] +#endif + public override HashAlgorithmType? SslHashAlgorithm { + get { + if (engine.IsSecure && (engine.Stream.Stream is SslStream sslStream)) + return sslStream.HashAlgorithm; + + return null; + } + } + + /// + /// Get the negotiated SSL or TLS hash algorithm strength. + /// + /// + /// Gets the negotiated SSL or TLS hash algorithm strength once an SSL or TLS connection has been made. + /// + /// + /// + /// + /// The negotiated SSL or TLS hash algorithm strength. +#if NET10_0_OR_GREATER + [Obsolete ("Use SslCipherSuite instead.")] +#endif + public override int? SslHashStrength { + get { + if (engine.IsSecure && (engine.Stream.Stream is SslStream sslStream)) + return sslStream.HashStrength; + + return null; + } + } + + /// + /// Get the negotiated SSL or TLS key exchange algorithm. + /// + /// + /// Gets the negotiated SSL or TLS key exchange algorithm once an SSL or TLS connection has been made. + /// + /// + /// + /// + /// The negotiated SSL or TLS key exchange algorithm. +#if NET10_0_OR_GREATER + [Obsolete ("Use SslCipherSuite instead.")] +#endif + public override ExchangeAlgorithmType? SslKeyExchangeAlgorithm { + get { + if (engine.IsSecure && (engine.Stream.Stream is SslStream sslStream)) + return sslStream.KeyExchangeAlgorithm; + + return null; + } + } + + /// + /// Get the negotiated SSL or TLS key exchange algorithm strength. + /// + /// + /// Gets the negotiated SSL or TLS key exchange algorithm strength once an SSL or TLS connection has been made. + /// + /// + /// + /// + /// The negotiated SSL or TLS key exchange algorithm strength. +#if NET10_0_OR_GREATER + [Obsolete ("Use SslCipherSuite instead.")] +#endif + public override int? SslKeyExchangeStrength { + get { + if (engine.IsSecure && (engine.Stream.Stream is SslStream sslStream)) + return sslStream.KeyExchangeStrength; + + return null; + } } /// @@ -400,45 +605,49 @@ public override bool IsSecure { /// To authenticate with the POP3 server, use one of the /// Authenticate methods. /// - /// true if the client is connected; otherwise, false. + /// if the client is authenticated; otherwise, . public override bool IsAuthenticated { get { return engine.State == Pop3EngineState.Transaction; } } - void UpdateMessageCount (CancellationToken cancellationToken) + Task ProcessStatResponse (Pop3Engine engine, Pop3Command pc, string text, bool doAsync, CancellationToken cancellationToken) { - var pc = engine.QueueCommand (cancellationToken, (pop3, cmd, text) => { - if (cmd.Status != Pop3CommandStatus.Ok) - return; + if (pc.Status != Pop3CommandStatus.Ok) + return Task.CompletedTask; - // the response should be " " - var tokens = text.Split (new [] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + // the response should be " " + var tokens = text.Split (Space, StringSplitOptions.RemoveEmptyEntries); - if (tokens.Length < 2) { - cmd.Exception = CreatePop3ParseException ("Pop3 server returned an incomplete response to the STAT command."); - return; - } + if (tokens.Length < 2) { + pc.Exception = CreatePop3ParseException ("Pop3 server returned an incomplete response to the STAT command: {0}", text); + return Task.CompletedTask; + } - if (!int.TryParse (tokens[0], out total)) { - cmd.Exception = CreatePop3ParseException ("Pop3 server returned an invalid response to the STAT command."); - return; - } - }, "STAT"); + if (!int.TryParse (tokens[0], NumberStyles.None, CultureInfo.InvariantCulture, out total) || total < 0) { + pc.Exception = CreatePop3ParseException ("Pop3 server returned an invalid response to the STAT command: {0}", text); + return Task.CompletedTask; + } - while (engine.Iterate () < pc.Id) { - // continue processing commands + if (!long.TryParse (tokens[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out octets)) { + pc.Exception = CreatePop3ParseException ("Pop3 server returned an invalid response to the STAT command: {0}", text); + return Task.CompletedTask; } - if (pc.Status != Pop3CommandStatus.Ok) - throw CreatePop3Exception (pc); + return Task.CompletedTask; + } + + int UpdateMessageCount (CancellationToken cancellationToken) + { + engine.QueueCommand (ProcessStatResponse, "STAT\r\n"); + + engine.Run (true, cancellationToken); - if (pc.Exception != null) - throw pc.Exception; + return Count; } void ProbeCapabilities (CancellationToken cancellationToken) { - if ((engine.Capabilities & Pop3Capabilities.UIDL) == 0) { + if ((engine.Capabilities & Pop3Capabilities.UIDL) == 0 && (probed & ProbedCapabilities.UIDL) == 0) { // if the message count is > 0, we can probe the UIDL command if (total > 0) { try { @@ -449,212 +658,390 @@ void ProbeCapabilities (CancellationToken cancellationToken) } } - /// - /// Authenticates using the supplied credentials. - /// - /// - /// If the POP3 server supports the APOP authentication mechanism, - /// then APOP is used. - /// If the APOP authentication mechanism is not supported and the - /// server supports one or more SASL authentication mechanisms, then - /// the SASL mechanisms that both the client and server support are tried - /// in order of greatest security to weakest security. Once a SASL - /// authentication mechanism is found that both client and server support, - /// the credentials are used to authenticate. - /// If the server does not support SASL or if no common SASL mechanisms - /// can be found, then the USER and PASS commands are used as a - /// fallback. - /// To prevent the usage of certain authentication mechanisms, - /// simply remove them from the hash set - /// before calling this method. - /// In the case of the APOP authentication mechanism, remove it from the - /// property instead. - /// - /// The text encoding to use for the user's credentials. - /// The user's credentials. - /// The cancellation token. - /// - /// is null. - /// -or- - /// is null. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is already authenticated. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// Authentication using the supplied credentials has failed. - /// - /// - /// A SASL authentication error occurred. - /// - /// - /// An I/O error occurred. - /// - /// - /// A POP3 command failed. - /// - /// - /// An POP3 protocol error occurred. - /// - public override void Authenticate (Encoding encoding, ICredentials credentials, CancellationToken cancellationToken = default (CancellationToken)) + class SaslAuthContext { - if (encoding == null) - throw new ArgumentNullException (nameof (encoding)); - - if (credentials == null) - throw new ArgumentNullException (nameof (credentials)); + readonly SaslMechanism mechanism; + readonly Pop3Client client; - if (!IsConnected) - throw new ServiceNotConnectedException ("The Pop3Client must be connected before you can authenticate."); + public SaslAuthContext (Pop3Client client, SaslMechanism mechanism) + { + this.mechanism = mechanism; + this.client = client; + } - if (IsAuthenticated) - throw new InvalidOperationException ("The Pop3Client is already authenticated."); + public string? AuthMessage { + get; private set; + } - CheckDisposed (); + Pop3Engine Engine { + get { return client.engine; } + } - var uri = new Uri ("pop://" + engine.Uri.Host); - string authMessage = string.Empty; - string userName, password; - NetworkCredential cred; - string challenge; - Pop3Command pc; + void OnDataReceived (Pop3Engine pop3, Pop3Command pc, string text, CancellationToken cancellationToken) + { + pop3.CheckConnected (); - if ((engine.Capabilities & Pop3Capabilities.Apop) != 0) { - cred = credentials.GetCredential (uri, "APOP"); - userName = utf8 ? SaslMechanism.SaslPrep (cred.UserName) : cred.UserName; - password = utf8 ? SaslMechanism.SaslPrep (cred.Password) : cred.Password; - challenge = engine.ApopToken + password; - var md5sum = new StringBuilder (); - byte[] digest; + while (pc.Status == Pop3CommandStatus.Continue && !mechanism.IsAuthenticated) { + var challenge = mechanism.Challenge (text, cancellationToken); + var buf = Encoding.ASCII.GetBytes (challenge + "\r\n"); - using (var md5 = MD5.Create ()) - digest = md5.ComputeHash (encoding.GetBytes (challenge)); + pop3.Stream.Write (buf, 0, buf.Length, cancellationToken); + pop3.Stream.Flush (cancellationToken); - for (int i = 0; i < digest.Length; i++) - md5sum.Append (digest[i].ToString ("x2")); + var response = pop3.ReadLine (cancellationToken).TrimEnd (); + pc.Status = Pop3Engine.GetCommandStatus (response, out text); + pc.StatusText = text; - try { - authMessage = SendCommand (cancellationToken, encoding, "APOP {0} {1}", userName, md5sum); - engine.State = Pop3EngineState.Transaction; - } catch (Pop3CommandException) { + if (pc.Status == Pop3CommandStatus.ProtocolError) + throw new Pop3ProtocolException (string.Format ("Unexpected response from server: {0}", response)); } - if (engine.State == Pop3EngineState.Transaction) { - engine.QueryCapabilities (cancellationToken); - UpdateMessageCount (cancellationToken); - ProbeCapabilities (cancellationToken); - OnAuthenticated (authMessage); - return; - } + AuthMessage = text; } - if ((engine.Capabilities & Pop3Capabilities.Sasl) != 0) { - foreach (var authmech in SaslMechanism.AuthMechanismRank) { - SaslMechanism sasl; - - if (!engine.AuthenticationMechanisms.Contains (authmech)) - continue; - - if ((sasl = SaslMechanism.Create (authmech, uri, credentials)) == null) - continue; + async Task OnDataReceivedAsync (Pop3Engine pop3, Pop3Command pc, string text, CancellationToken cancellationToken) + { + pop3.CheckConnected (); - cancellationToken.ThrowIfCancellationRequested (); + while (pc.Status == Pop3CommandStatus.Continue && !mechanism.IsAuthenticated) { + var challenge = await mechanism.ChallengeAsync (text, cancellationToken).ConfigureAwait (false); + var buf = Encoding.ASCII.GetBytes (challenge + "\r\n"); - pc = engine.QueueCommand (cancellationToken, (pop3, cmd, text) => { - if (sasl.IsAuthenticated) { - if (cmd.Status == Pop3CommandStatus.Ok) - authMessage = text; - return; - } + await pop3.Stream.WriteAsync (buf, 0, buf.Length, cancellationToken).ConfigureAwait (false); + await pop3.Stream.FlushAsync (cancellationToken).ConfigureAwait (false); - while (!sasl.IsAuthenticated) { - challenge = sasl.Challenge (text); + var response = (await pop3.ReadLineAsync (cancellationToken).ConfigureAwait (false)).TrimEnd (); + pc.Status = Pop3Engine.GetCommandStatus (response, out text); + pc.StatusText = text; - var buf = Encoding.ASCII.GetBytes (challenge + "\r\n"); - pop3.Stream.Write (buf, 0, buf.Length, cmd.CancellationToken); - pop3.Stream.Flush (cmd.CancellationToken); + if (pc.Status == Pop3CommandStatus.ProtocolError) + throw new Pop3ProtocolException (string.Format ("Unexpected response from server: {0}", response)); + } - var response = pop3.ReadLine (cmd.CancellationToken).TrimEnd (); + AuthMessage = text; + } - cmd.Status = Pop3Engine.GetCommandStatus (response, out text); - cmd.StatusText = text; + Task OnDataReceived (Pop3Engine pop3, Pop3Command pc, string text, bool doAsync, CancellationToken cancellationToken) + { + if (doAsync) + return OnDataReceivedAsync (pop3, pc, text, cancellationToken); - if (cmd.Status == Pop3CommandStatus.ProtocolError) - throw new Pop3ProtocolException (string.Format ("Unexpected response from server: {0}", response)); - } - }, "AUTH {0}", authmech); + OnDataReceived (pop3, pc, text, cancellationToken); + return Task.CompletedTask; + } - while (engine.Iterate () < pc.Id) { - // continue processing commands - } + public Pop3Command Authenticate (CancellationToken cancellationToken) + { + var pc = Engine.QueueCommand (OnDataReceived, "AUTH {0}\r\n", mechanism.MechanismName); - if (pc.Status == Pop3CommandStatus.Error) - continue; + AuthMessage = string.Empty; - if (pc.Status != Pop3CommandStatus.Ok) - throw CreatePop3Exception (pc); + client.detector.IsAuthenticating = true; - if (pc.Exception != null) - throw pc.Exception; - - engine.State = Pop3EngineState.Transaction; - engine.QueryCapabilities (cancellationToken); - UpdateMessageCount (cancellationToken); - ProbeCapabilities (cancellationToken); - OnAuthenticated (authMessage); - return; + try { + // Note: We defer throwing exceptions on command failure so that our caller can continue trying other authentication mechanisms. + Engine.Run (false, cancellationToken); + } finally { + client.detector.IsAuthenticating = false; } + + return pc; } - // fall back to the classic USER & PASS commands... - cred = credentials.GetCredential (uri, "DEFAULT"); - userName = utf8 ? SaslMechanism.SaslPrep (cred.UserName) : cred.UserName; - password = utf8 ? SaslMechanism.SaslPrep (cred.Password) : cred.Password; + public async Task AuthenticateAsync (CancellationToken cancellationToken) + { + var pc = Engine.QueueCommand (OnDataReceived, "AUTH {0}\r\n", mechanism.MechanismName); - try { - SendCommand (cancellationToken, encoding, "USER {0}", userName); - authMessage = SendCommand (cancellationToken, encoding, "PASS {0}", password); - } catch (Pop3CommandException) { - throw new AuthenticationException (); + AuthMessage = string.Empty; + + client.detector.IsAuthenticating = true; + + try { + // Note: We defer throwing exceptions on command failure so that our caller can continue trying other authentication mechanisms. + await Engine.RunAsync (false, cancellationToken).ConfigureAwait (false); + } finally { + client.detector.IsAuthenticating = false; + } + + return pc; } + } + + Uri CheckCanAuthenticate (SaslMechanism mechanism, CancellationToken cancellationToken) + { + if (mechanism == null) + throw new ArgumentNullException (nameof (mechanism)); + + if (!engine.IsConnected) + throw new ServiceNotConnectedException ("The Pop3Client must be connected before you can authenticate."); + + if (IsAuthenticated) + throw new InvalidOperationException ("The Pop3Client is already authenticated."); + + CheckDisposed (); + + cancellationToken.ThrowIfCancellationRequested (); + return new Uri ("pop://" + engine.Uri.Host); + } + + SaslAuthContext GetSaslAuthContext (SaslMechanism mechanism, Uri saslUri) + { + mechanism.ChannelBindingContext = engine.Stream!.Stream as IChannelBindingContext; + mechanism.Uri = saslUri; + + return new SaslAuthContext (this, mechanism); + } + + void OnAuthenticated (string message, CancellationToken cancellationToken) + { engine.State = Pop3EngineState.Transaction; + engine.QueryCapabilities (cancellationToken); UpdateMessageCount (cancellationToken); ProbeCapabilities (cancellationToken); - OnAuthenticated (authMessage); + OnAuthenticated (message); } - internal void ReplayConnect (string host, Stream replayStream, CancellationToken cancellationToken = default (CancellationToken)) + /// + /// Authenticate using the specified SASL mechanism. + /// + /// + /// Authenticates using the specified SASL mechanism. + /// For a list of available SASL authentication mechanisms supported by the server, + /// check the property after the service has been + /// connected. + /// + /// The SASL mechanism. + /// The cancellation token. + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is already authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Authentication using the supplied credentials has failed. + /// + /// + /// A SASL authentication error occurred. + /// + /// + /// An I/O error occurred. + /// + /// + /// A POP3 command failed. + /// + /// + /// An POP3 protocol error occurred. + /// + public override void Authenticate (SaslMechanism mechanism, CancellationToken cancellationToken = default) { - if (host == null) - throw new ArgumentNullException (nameof (host)); + var saslUri = CheckCanAuthenticate (mechanism, cancellationToken); + + using var operation = engine.StartNetworkOperation (NetworkOperationKind.Authenticate); + + try { + var ctx = GetSaslAuthContext (mechanism, saslUri); + + var pc = ctx.Authenticate (cancellationToken); + + if (pc.Status == Pop3CommandStatus.Error) + throw new AuthenticationException (); - if (replayStream == null) - throw new ArgumentNullException (nameof (replayStream)); + pc.ThrowIfError (); + + OnAuthenticated (ctx.AuthMessage!, cancellationToken); + } catch (Exception ex) { + operation.SetError (ex); + throw; + } + } + + Uri CheckCanAuthenticate (Encoding encoding, ICredentials credentials, CancellationToken cancellationToken) + { + if (encoding == null) + throw new ArgumentNullException (nameof (encoding)); + + if (credentials == null) + throw new ArgumentNullException (nameof (credentials)); + + if (!engine.IsConnected) + throw new ServiceNotConnectedException ("The Pop3Client must be connected before you can authenticate."); + + if (IsAuthenticated) + throw new InvalidOperationException ("The Pop3Client is already authenticated."); CheckDisposed (); - probed = ProbedCapabilities.None; - secure = false; + cancellationToken.ThrowIfCancellationRequested (); - engine.Uri = new Uri ("pop://" + host); - engine.Connect (new Pop3Stream (replayStream, null, ProtocolLogger), cancellationToken); - engine.QueryCapabilities (cancellationToken); - engine.Disconnected += OnEngineDisconnected; - OnConnected (); + return new Uri ("pop://" + engine.Uri.Host); } - static void ComputeDefaultValues (string host, ref int port, ref SecureSocketOptions options, out Uri uri, out bool starttls) + string GetApopCommand (Encoding encoding, NetworkCredential cred) + { + var userName = utf8 ? SaslMechanism.SaslPrep (cred.UserName) : cred.UserName; + var password = utf8 ? SaslMechanism.SaslPrep (cred.Password) : cred.Password; + var challenge = engine.ApopToken + password; + var md5sum = new StringBuilder (); + byte[] digest; + + using (var md5 = MD5.Create ()) + digest = md5.ComputeHash (encoding.GetBytes (challenge)); + + for (int i = 0; i < digest.Length; i++) + md5sum.Append (digest[i].ToString ("x2")); + + return $"APOP {userName} {md5sum}\r\n"; + } + + /// + /// Authenticate using the supplied credentials. + /// + /// + /// Authenticates using the supplied credentials. + /// If the POP3 server supports the APOP authentication mechanism, + /// then APOP is used. + /// If the APOP authentication mechanism is not supported and the + /// server supports one or more SASL authentication mechanisms, then + /// the SASL mechanisms that both the client and server support (not including + /// any OAUTH mechanisms) are tried in order of greatest security to weakest + /// security. Once a SASL authentication mechanism is found that both client + /// and server support, the credentials are used to authenticate. + /// If the server does not support SASL or if no common SASL mechanisms + /// can be found, then the USER and PASS commands are used as a + /// fallback. + /// To prevent the usage of certain authentication mechanisms, + /// simply remove them from the hash set + /// before calling this method. + /// In the case of the APOP authentication mechanism, remove it from the + /// property instead. + /// + /// The text encoding to use for the user's credentials. + /// The user's credentials. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is already authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Authentication using the supplied credentials has failed. + /// + /// + /// A SASL authentication error occurred. + /// + /// + /// An I/O error occurred. + /// + /// + /// A POP3 command failed. + /// + /// + /// An POP3 protocol error occurred. + /// + public override void Authenticate (Encoding encoding, ICredentials credentials, CancellationToken cancellationToken = default) + { + var saslUri = CheckCanAuthenticate (encoding, credentials, cancellationToken); + + using var operation = engine.StartNetworkOperation (NetworkOperationKind.Authenticate); + + try { + string userName, password; + NetworkCredential? cred; + string? message = null; + + if ((engine.Capabilities & Pop3Capabilities.Apop) != 0 && (cred = credentials.GetCredential (saslUri, "APOP")) != null) { + var apop = GetApopCommand (encoding, cred); + + detector.IsAuthenticating = true; + + try { + message = SendCommand (cancellationToken, encoding, apop); + engine.State = Pop3EngineState.Transaction; + } catch (Pop3CommandException) { + } finally { + detector.IsAuthenticating = false; + } + + if (engine.State == Pop3EngineState.Transaction) { + OnAuthenticated (message ?? string.Empty, cancellationToken); + return; + } + } + + if ((engine.Capabilities & Pop3Capabilities.Sasl) != 0) { + foreach (var authmech in SaslMechanism.Rank (engine.AuthenticationMechanisms)) { + SaslMechanism? sasl; + + cred = credentials.GetCredential (saslUri, authmech); + + if (cred == null || (sasl = SaslMechanism.Create (authmech, encoding, cred)) == null) + continue; + + cancellationToken.ThrowIfCancellationRequested (); + + var ctx = GetSaslAuthContext (sasl, saslUri); + + var pc = ctx.Authenticate (cancellationToken); + + if (pc.Status == Pop3CommandStatus.Error) + continue; + + pc.ThrowIfError (); + + OnAuthenticated (ctx.AuthMessage!, cancellationToken); + return; + } + } + + // fall back to the classic USER & PASS commands... + if ((cred = credentials.GetCredential (saslUri, "DEFAULT")) == null) + throw new AuthenticationException ("No credentials could be found for the POP3 server."); + + userName = utf8 ? SaslMechanism.SaslPrep (cred.UserName) : cred.UserName; + password = utf8 ? SaslMechanism.SaslPrep (cred.Password) : cred.Password; + detector.IsAuthenticating = true; + + try { + SendCommand (cancellationToken, encoding, "USER {0}\r\n", userName); + message = SendCommand (cancellationToken, encoding, "PASS {0}\r\n", password); + } catch (Pop3CommandException) { + throw new AuthenticationException (); + } finally { + detector.IsAuthenticating = false; + } + + OnAuthenticated (message, cancellationToken); + } catch (Exception ex) { + operation.SetError (ex); + throw; + } + } + + internal static void ComputeDefaultValues (string host, ref int port, ref SecureSocketOptions options, out Uri uri, out bool starttls) { switch (options) { default: @@ -674,88 +1061,30 @@ static void ComputeDefaultValues (string host, ref int port, ref SecureSocketOpt break; } + if (IPAddress.TryParse (host, out var ip) && ip.AddressFamily == AddressFamily.InterNetworkV6) + host = "[" + host + "]"; + switch (options) { case SecureSocketOptions.StartTlsWhenAvailable: - uri = new Uri ("pop://" + host + ":" + port + "/?starttls=when-available"); + uri = new Uri (string.Format (CultureInfo.InvariantCulture, "pop://{0}:{1}/?starttls=when-available", host, port)); starttls = true; break; case SecureSocketOptions.StartTls: - uri = new Uri ("pop://" + host + ":" + port + "/?starttls=always"); + uri = new Uri (string.Format (CultureInfo.InvariantCulture, "pop://{0}:{1}/?starttls=always", host, port)); starttls = true; break; case SecureSocketOptions.SslOnConnect: - uri = new Uri ("pops://" + host + ":" + port); + uri = new Uri (string.Format (CultureInfo.InvariantCulture, "pops://{0}:{1}", host, port)); starttls = false; break; default: - uri = new Uri ("pop://" + host + ":" + port); + uri = new Uri (string.Format (CultureInfo.InvariantCulture, "pop://{0}:{1}", host, port)); starttls = false; break; } } - /// - /// Establish a connection to the specified POP3 or POP3/S server. - /// - /// - /// Establishes a connection to the specified POP3 or POP3/S server. - /// If the has a value of 0, then the - /// parameter is used to determine the default port to - /// connect to. The default port used with - /// is 995. All other values will use a default port of 110. - /// If the has a value of - /// , then the is used - /// to determine the default security options. If the has a value - /// of 995, then the default options used will be - /// . All other values will use - /// . - /// Once a connection is established, properties such as - /// and will be - /// populated. - /// - /// - /// - /// - /// The host name to connect to. - /// The port to connect to. If the specified port is 0, then the default port will be used. - /// The secure socket options to when connecting. - /// The cancellation token. - /// - /// is null. - /// - /// - /// is not between 0 and 65535. - /// - /// - /// The is a zero-length string. - /// - /// - /// The has been disposed. - /// - /// - /// The is already connected. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// was set to - /// - /// and the POP3 server does not support the STLS extension. - /// - /// - /// A socket error occurred trying to connect to the remote host. - /// - /// - /// An I/O error occurred. - /// - /// - /// A POP3 command failed. - /// - /// - /// A POP3 protocol error occurred. - /// - public override void Connect (string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default (CancellationToken)) + void CheckCanConnect (string host, int port) { if (host == null) throw new ArgumentNullException (nameof (host)); @@ -770,145 +1099,69 @@ static void ComputeDefaultValues (string host, ref int port, ref SecureSocketOpt if (IsConnected) throw new InvalidOperationException ("The Pop3Client is already connected."); + } - Stream stream; - bool starttls; - Uri uri; - - ComputeDefaultValues (host, ref port, ref options, out uri, out starttls); - -#if !NETFX_CORE -#if NETSTANDARD - var ipAddresses = Dns.GetHostAddressesAsync (uri.DnsSafeHost).GetAwaiter ().GetResult (); -#else - var ipAddresses = Dns.GetHostAddresses (uri.DnsSafeHost); -#endif - Socket socket = null; - - for (int i = 0; i < ipAddresses.Length; i++) { - socket = new Socket (ipAddresses[i].AddressFamily, SocketType.Stream, ProtocolType.Tcp); - - try { - cancellationToken.ThrowIfCancellationRequested (); - - if (LocalEndPoint != null) - socket.Bind (LocalEndPoint); - - socket.Connect (ipAddresses[i], port); - break; - } catch (OperationCanceledException) { - socket.Dispose (); - throw; - } catch { - socket.Dispose (); - - if (i + 1 == ipAddresses.Length) - throw; - } - } - - if (socket == null) - throw new IOException (string.Format ("Failed to resolve host: {0}", host)); - - engine.Uri = uri; - - if (options == SecureSocketOptions.SslOnConnect) { - var ssl = new SslStream (new NetworkStream (socket, true), false, ValidateRemoteCertificate); - - try { -#if NETSTANDARD - ssl.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, true).GetAwaiter ().GetResult (); + void SslHandshake (SslStream ssl, string host, CancellationToken cancellationToken) + { +#if NET5_0_OR_GREATER + ssl.AuthenticateAsClient (GetSslClientAuthenticationOptions (host, ValidateRemoteCertificate)); #else - ssl.AuthenticateAsClient (host, ClientCertificates, SslProtocols, true); + ssl.AuthenticateAsClient (host, ClientCertificates, SslProtocols, CheckCertificateRevocation); #endif - } catch { - ssl.Dispose (); - throw; - } + } - secure = true; - stream = ssl; - } else { - stream = new NetworkStream (socket, true); - secure = false; - } -#else - var protection = options == SecureSocketOptions.SslOnConnect ? SocketProtectionLevel.Tls12 : SocketProtectionLevel.PlainSocket; - socket = new StreamSocket (); + void PostConnect (Stream stream, string host, int port, SecureSocketOptions options, bool starttls, CancellationToken cancellationToken) + { + probed = ProbedCapabilities.None; try { - cancellationToken.ThrowIfCancellationRequested (); - socket.ConnectAsync (new HostName (host), port.ToString (), protection) - .AsTask (cancellationToken) - .GetAwaiter () - .GetResult (); + ProtocolLogger.LogConnect (engine.Uri!); } catch { - socket.Dispose (); - socket = null; + stream.Dispose (); throw; } - stream = new DuplexStream (socket.InputStream.AsStreamForRead (0), socket.OutputStream.AsStreamForWrite (0)); - secure = options == SecureSocketOptions.SslOnConnect; - engine.Uri = uri; -#endif - - probed = ProbedCapabilities.None; - if (stream.CanTimeout) { - stream.WriteTimeout = timeout; - stream.ReadTimeout = timeout; - } - - ProtocolLogger.LogConnect (uri); + var pop3 = new Pop3Stream (stream, ProtocolLogger); - engine.Connect (new Pop3Stream (stream, socket, ProtocolLogger), cancellationToken); + engine.Connect (pop3, cancellationToken); try { engine.QueryCapabilities (cancellationToken); if (options == SecureSocketOptions.StartTls && (engine.Capabilities & Pop3Capabilities.StartTLS) == 0) throw new NotSupportedException ("The POP3 server does not support the STLS extension."); - + if (starttls && (engine.Capabilities & Pop3Capabilities.StartTLS) != 0) { - SendCommand (cancellationToken, "STLS"); + SendCommand (cancellationToken, "STLS\r\n"); -#if !NETFX_CORE - var tls = new SslStream (stream, false, ValidateRemoteCertificate); -#if NETSTANDARD - tls.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, true).GetAwaiter ().GetResult (); -#else - tls.AuthenticateAsClient (host, ClientCertificates, SslProtocols, true); -#endif - engine.Stream.Stream = tls; -#else - socket.UpgradeToSslAsync (SocketProtectionLevel.Tls12, new HostName (host)) - .AsTask (cancellationToken) - .GetAwaiter () - .GetResult (); -#endif + try { + var tls = new ExtendedSslStream (stream, false, ValidateRemoteCertificate); + pop3.SetStream (tls); + + SslHandshake (tls, host, cancellationToken); + } catch (Exception ex) { + throw SslHandshakeException.Create (ref sslValidationInfo, ex, true, "POP3", host, port, 995, 110); + } - secure = true; + engine.IsSecure = true; // re-issue a CAPA command engine.QueryCapabilities (cancellationToken); } - } catch { - engine.Disconnect (); - secure = false; + } catch (Exception ex) { + engine.Disconnect (ex); throw; } engine.Disconnected += OnEngineDisconnected; - OnConnected (); + OnConnected (host, port, options); } -#if !NETFX_CORE /// - /// Establish a connection to the specified POP3 or POP3/S server using the provided socket. + /// Establish a connection to the specified POP3 or POP3/S server. /// /// - /// Establishes a connection to the specified POP3 or POP3/S server using - /// the provided socket. + /// Establishes a connection to the specified POP3 or POP3/S server. /// If the has a value of 0, then the /// parameter is used to determine the default port to /// connect to. The default port used with @@ -923,22 +1176,20 @@ static void ComputeDefaultValues (string host, ref int port, ref SecureSocketOpt /// and will be /// populated. /// - /// The socket to use for the connection. + /// + /// + /// /// The host name to connect to. /// The port to connect to. If the specified port is 0, then the default port will be used. /// The secure socket options to when connecting. /// The cancellation token. /// - /// is null. - /// -or- - /// is null. + /// is . /// /// /// is not between 0 and 65535. /// /// - /// is not connected. - /// -or- /// The is a zero-length string. /// /// @@ -947,13 +1198,19 @@ static void ComputeDefaultValues (string host, ref int port, ref SecureSocketOpt /// /// The is already connected. /// + /// + /// The operation was canceled via the cancellation token. + /// /// /// was set to /// /// and the POP3 server does not support the STLS extension. /// - /// - /// The operation was canceled via the cancellation token. + /// + /// A socket error occurred trying to connect to the remote host. + /// + /// + /// An error occurred during the SSL/TLS negotiations. /// /// /// An I/O error occurred. @@ -964,246 +1221,277 @@ static void ComputeDefaultValues (string host, ref int port, ref SecureSocketOpt /// /// A POP3 protocol error occurred. /// - public void Connect (Socket socket, string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default (CancellationToken)) + public override void Connect (string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default) { - if (socket == null) - throw new ArgumentNullException (nameof (socket)); - - if (!socket.Connected) - throw new ArgumentException ("The socket is not connected.", nameof (socket)); - - if (host == null) - throw new ArgumentNullException (nameof (host)); - - if (host.Length == 0) - throw new ArgumentException ("The host name cannot be empty.", nameof (host)); + CheckCanConnect (host, port); - if (port < 0 || port > 65535) - throw new ArgumentOutOfRangeException (nameof (port)); - - CheckDisposed (); - - if (IsConnected) - throw new InvalidOperationException ("The Pop3Client is already connected."); - - Stream stream; - bool starttls; - Uri uri; - - ComputeDefaultValues (host, ref port, ref options, out uri, out starttls); - - engine.Uri = uri; - - if (options == SecureSocketOptions.SslOnConnect) { - var ssl = new SslStream (new NetworkStream (socket, true), false, ValidateRemoteCertificate); - - try { -#if NETSTANDARD - ssl.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, true).GetAwaiter ().GetResult (); -#else - ssl.AuthenticateAsClient (host, ClientCertificates, SslProtocols, true); -#endif - } catch { - ssl.Dispose (); - throw; - } + ComputeDefaultValues (host, ref port, ref options, out var uri, out var starttls); - secure = true; - stream = ssl; - } else { - stream = new NetworkStream (socket, true); - secure = false; - } + using var operation = engine.StartNetworkOperation (NetworkOperationKind.Connect, uri); - probed = ProbedCapabilities.None; - if (stream.CanTimeout) { + try { + var stream = ConnectNetwork (host, port, cancellationToken); stream.WriteTimeout = timeout; stream.ReadTimeout = timeout; - } - - ProtocolLogger.LogConnect (uri); - engine.Connect (new Pop3Stream (stream, socket, ProtocolLogger), cancellationToken); + engine.Uri = uri; - try { - engine.QueryCapabilities (cancellationToken); - - if (options == SecureSocketOptions.StartTls && (engine.Capabilities & Pop3Capabilities.StartTLS) == 0) - throw new NotSupportedException ("The POP3 server does not support the STLS extension."); - - if (starttls && (engine.Capabilities & Pop3Capabilities.StartTLS) != 0) { - SendCommand (cancellationToken, "STLS"); + if (options == SecureSocketOptions.SslOnConnect) { + var ssl = new ExtendedSslStream (stream, false, ValidateRemoteCertificate); - var tls = new SslStream (stream, false, ValidateRemoteCertificate); -#if NETSTANDARD - tls.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, true).GetAwaiter ().GetResult (); -#else - tls.AuthenticateAsClient (host, ClientCertificates, SslProtocols, true); -#endif - engine.Stream.Stream = tls; + try { + SslHandshake (ssl, host, cancellationToken); + } catch (Exception ex) { + ssl.Dispose (); - secure = true; + throw SslHandshakeException.Create (ref sslValidationInfo, ex, false, "POP3", host, port, 995, 110); + } - // re-issue a CAPA command - engine.QueryCapabilities (cancellationToken); + stream = ssl; } - } catch { - engine.Disconnect (); - secure = false; + + PostConnect (stream, host, port, options, starttls, cancellationToken); + } catch (Exception ex) { + operation.SetError (ex); throw; } - - engine.Disconnected += OnEngineDisconnected; - OnConnected (); } -#endif - /// - /// Disconnect the service. - /// - /// - /// If is true, a QUIT command will be issued in order to disconnect cleanly. - /// - /// - /// - /// - /// If set to true, a QUIT command will be issued in order to disconnect cleanly. - /// The cancellation token. - /// - /// The has been disposed. - /// - public override void Disconnect (bool quit, CancellationToken cancellationToken = default (CancellationToken)) + void CheckCanConnect (Stream stream, string host, int port) { - CheckDisposed (); - - if (!engine.IsConnected) - return; + if (stream == null) + throw new ArgumentNullException (nameof (stream)); - if (quit) { - try { - SendCommand (cancellationToken, "QUIT"); - } catch (OperationCanceledException) { - } catch (Pop3ProtocolException) { - } catch (Pop3CommandException) { - } catch (IOException) { - } - } + CheckCanConnect (host, port); + } -#if NETFX_CORE - socket.Dispose (); - socket = null; -#endif + void CheckCanConnect (Socket socket, string host, int port) + { + if (socket == null) + throw new ArgumentNullException (nameof (socket)); - secure = utf8 = false; - dict.Clear (); - total = 0; + if (!socket.Connected) + throw new ArgumentException ("The socket is not connected.", nameof (socket)); - engine.Disconnect (); + CheckCanConnect (host, port); } /// - /// Ping the POP3 server to keep the connection alive. + /// Establish a connection to the specified POP3 or POP3/S server using the provided socket. /// - /// Mail servers, if left idle for too long, will automatically drop the connection. + /// + /// Establishes a connection to the specified POP3 or POP3/S server using + /// the provided socket. + /// If the has a value of + /// , then the is used + /// to determine the default security options. If the has a value + /// of 995, then the default options used will be + /// . All other values will use + /// . + /// Once a connection is established, properties such as + /// and will be + /// populated. + /// With the exception of using the to determine the + /// default to use when the value + /// is , the and + /// parameters are only used for logging purposes. + /// + /// The socket to use for the connection. + /// The host name to connect to. + /// The port to connect to. If the specified port is 0, then the default port will be used. + /// The secure socket options to when connecting. /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// is not between 0 and 65535. + /// + /// + /// is not connected. + /// -or- + /// The is a zero-length string. + /// /// /// The has been disposed. /// - /// - /// The is not connected. + /// + /// The is already connected. /// - /// - /// The is not authenticated. + /// + /// was set to + /// + /// and the POP3 server does not support the STLS extension. /// /// /// The operation was canceled via the cancellation token. /// + /// + /// An error occurred during the SSL/TLS negotiations. + /// /// /// An I/O error occurred. /// /// - /// The POP3 command failed. + /// A POP3 command failed. /// /// /// A POP3 protocol error occurred. /// - public override void NoOp (CancellationToken cancellationToken = default (CancellationToken)) - { - CheckDisposed (); - CheckConnected (); - CheckAuthenticated (); - - SendCommand (cancellationToken, "NOOP"); - } - - void OnEngineDisconnected (object sender, EventArgs e) + public override void Connect (Socket socket, string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default) { - engine.Disconnected -= OnEngineDisconnected; - secure = utf8 = false; + CheckCanConnect (socket, host, port); - OnDisconnected (); + Connect (new NetworkStream (socket, true), host, port, options, cancellationToken); } - #endregion - /// - /// Enable UTF8 mode. + /// Establish a connection to the specified POP3 or POP3/S server using the provided stream. /// /// - /// The POP3 UTF8 extension allows the client to retrieve messages in the UTF-8 encoding and - /// may also allow the user to authenticate using a UTF-8 encoded username or password. + /// Establishes a connection to the specified POP3 or POP3/S server using + /// the provided stream. + /// If the has a value of + /// , then the is used + /// to determine the default security options. If the has a value + /// of 995, then the default options used will be + /// . All other values will use + /// . + /// Once a connection is established, properties such as + /// and will be + /// populated. + /// With the exception of using the to determine the + /// default to use when the value + /// is , the and + /// parameters are only used for logging purposes. /// + /// The stream to use for the connection. + /// The host name to connect to. + /// The port to connect to. If the specified port is 0, then the default port will be used. + /// The secure socket options to when connecting. /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// /// /// The has been disposed. /// - /// - /// The is not connected. - /// /// - /// The has already been authenticated. + /// The is already connected. + /// + /// + /// was set to + /// + /// and the POP3 server does not support the STLS extension. /// /// /// The operation was canceled via the cancellation token. /// - /// - /// The POP3 server does not support the UTF8 extension. + /// + /// An error occurred during the SSL/TLS negotiations. /// /// /// An I/O error occurred. /// /// - /// The POP3 command failed. + /// A POP3 command failed. /// /// /// A POP3 protocol error occurred. /// - public void EnableUTF8 (CancellationToken cancellationToken = default (CancellationToken)) + public override void Connect (Stream stream, string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default) { - CheckDisposed (); - CheckConnected (); + CheckCanConnect (stream, host, port); - if (engine.State != Pop3EngineState.Connected) - throw new InvalidOperationException ("You must enable UTF-8 mode before authenticating."); + Stream network; - if ((engine.Capabilities & Pop3Capabilities.UTF8) == 0) - throw new NotSupportedException ("The POP3 server does not support the UTF8 extension."); + ComputeDefaultValues (host, ref port, ref options, out var uri, out var starttls); + + using var operation = engine.StartNetworkOperation (NetworkOperationKind.Connect, uri); + + try { + engine.Uri = uri; + + if (options == SecureSocketOptions.SslOnConnect) { + var ssl = new ExtendedSslStream (stream, false, ValidateRemoteCertificate); + + try { + SslHandshake (ssl, host, cancellationToken); + } catch (Exception ex) { + ssl.Dispose (); + + throw SslHandshakeException.Create (ref sslValidationInfo, ex, false, "POP3", host, port, 995, 110); + } + + network = ssl; + } else { + network = stream; + } + + if (network.CanTimeout) { + network.WriteTimeout = timeout; + network.ReadTimeout = timeout; + } + + PostConnect (network, host, port, options, starttls, cancellationToken); + } catch (Exception ex) { + operation.SetError (ex); + throw; + } + } + + /// + /// Disconnect the service. + /// + /// + /// If is , a QUIT command will be issued in order to disconnect cleanly. + /// + /// + /// + /// + /// If set to , a QUIT command will be issued in order to disconnect cleanly. + /// The cancellation token. + /// + /// The has been disposed. + /// + public override void Disconnect (bool quit, CancellationToken cancellationToken = default) + { + CheckDisposed (); - if (utf8) + if (!engine.IsConnected) return; - SendCommand (cancellationToken, "UTF8"); - utf8 = true; + if (quit) { + try { + SendCommand (cancellationToken, "QUIT\r\n"); + } catch (OperationCanceledException) { + } catch (Pop3ProtocolException) { + } catch (Pop3CommandException) { + } catch (IOException) { + } + } + + disconnecting = true; + engine.Disconnect (null); } /// - /// Asynchronously enable UTF8 mode. + /// Get the message count. /// /// - /// The POP3 UTF8 extension allows the client to retrieve messages in the UTF-8 encoding and - /// may also allow the user to authenticate using a UTF-8 encoded username or password. + /// Gets the message count. /// - /// An asynchronous task context. + /// The message count. /// The cancellation token. /// /// The has been disposed. @@ -1211,14 +1499,11 @@ void OnEngineDisconnected (object sender, EventArgs e) /// /// The is not connected. /// - /// - /// The has already been authenticated. - /// - /// - /// The operation was canceled via the cancellation token. + /// + /// The is not authenticated. /// - /// - /// The POP3 server does not support the UTF8 extension. + /// + /// The operation was canceled via the cancellation token. /// /// /// An I/O error occurred. @@ -1229,24 +1514,19 @@ void OnEngineDisconnected (object sender, EventArgs e) /// /// A POP3 protocol error occurred. /// - public Task EnableUTF8Async (CancellationToken cancellationToken = default (CancellationToken)) + public override int GetMessageCount (CancellationToken cancellationToken = default) { - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - EnableUTF8 (cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); + + return UpdateMessageCount (cancellationToken); } /// - /// Get the list of languages supported by the POP3 server. + /// Ping the POP3 server to keep the connection alive. /// - /// - /// If the POP3 server supports the LANG extension, it is possible to - /// query the list of languages supported by the POP3 server that can - /// be used for error messages. - /// - /// The supported languages. + /// Mail servers, if left idle for too long, will automatically drop the connection. /// The cancellation token. /// /// The has been disposed. @@ -1254,12 +1534,12 @@ void OnEngineDisconnected (object sender, EventArgs e) /// /// The is not connected. /// + /// + /// The is not authenticated. + /// /// /// The operation was canceled via the cancellation token. /// - /// - /// The POP3 server does not support the LANG extension. - /// /// /// An I/O error occurred. /// @@ -1269,55 +1549,60 @@ void OnEngineDisconnected (object sender, EventArgs e) /// /// A POP3 protocol error occurred. /// - public IList GetLanguages (CancellationToken cancellationToken = default (CancellationToken)) + public override void NoOp (CancellationToken cancellationToken = default) { CheckDisposed (); CheckConnected (); + CheckAuthenticated (); - if ((Capabilities & Pop3Capabilities.Lang) == 0) - throw new NotSupportedException ("The POP3 server does not support the LANG extension."); + SendCommand (cancellationToken, "NOOP\r\n"); + } - var langs = new List (); + void OnEngineDisconnected (object? sender, EventArgs e) + { + var options = SecureSocketOptions.None; + bool requested = disconnecting; + string? host = null; + int port = 0; - var pc = engine.QueueCommand (cancellationToken, (pop3, cmd, text) => { - if (cmd.Status != Pop3CommandStatus.Ok) - return; + if (engine.Uri != null) { + options = GetSecureSocketOptions (engine.Uri); + host = engine.Uri.Host; + port = engine.Uri.Port; + } - do { - var response = engine.ReadLine (cmd.CancellationToken); - if (response == ".") - break; + engine.Disconnected -= OnEngineDisconnected; + disconnecting = utf8 = false; + octets = total = 0; + engine.Uri = null; - var tokens = response.Split (new [] { ' ' }, 2); - if (tokens.Length != 2) - continue; + if (host != null) + OnDisconnected (host, port, options, requested); + } - langs.Add (new Pop3Language (tokens[0], tokens[1])); - } while (true); - }, "LANG"); + #endregion - while (engine.Iterate () < pc.Id) { - // continue processing commands - } + bool CheckCanEnableUTF8 () + { + CheckDisposed (); + CheckConnected (); - if (pc.Status != Pop3CommandStatus.Ok) - throw CreatePop3Exception (pc); + if (engine.State != Pop3EngineState.Connected) + throw new InvalidOperationException ("You must enable UTF-8 mode before authenticating."); - if (pc.Exception != null) - throw pc.Exception; + if ((engine.Capabilities & Pop3Capabilities.UTF8) == 0) + throw new NotSupportedException ("The POP3 server does not support the UTF8 extension."); - return new ReadOnlyCollection (langs); + return !utf8; } /// - /// Asynchronously get the list of languages supported by the POP3 server. + /// Enable UTF8 mode. /// /// - /// If the POP3 server supports the LANG extension, it is possible to - /// query the list of languages supported by the POP3 server that can - /// be used for error messages. + /// The POP3 UTF8 extension allows the client to retrieve messages in the UTF-8 encoding and + /// may also allow the user to authenticate using a UTF-8 encoded username or password. /// - /// The supported languages. /// The cancellation token. /// /// The has been disposed. @@ -1325,11 +1610,14 @@ void OnEngineDisconnected (object sender, EventArgs e) /// /// The is not connected. /// + /// + /// The has already been authenticated. + /// /// /// The operation was canceled via the cancellation token. /// /// - /// The POP3 server does not support the LANG extension. + /// The POP3 server does not support the UTF8 extension. /// /// /// An I/O error occurred. @@ -1340,30 +1628,70 @@ void OnEngineDisconnected (object sender, EventArgs e) /// /// A POP3 protocol error occurred. /// - public Task> GetLanguagesAsync (CancellationToken cancellationToken = default (CancellationToken)) + public void EnableUTF8 (CancellationToken cancellationToken = default) { - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return GetLanguages (cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); + if (!CheckCanEnableUTF8 ()) + return; + + SendCommand (cancellationToken, "UTF8\r\n"); + utf8 = true; + } + + static void ReadLangResponse (Pop3Engine engine, Pop3Command pc, CancellationToken cancellationToken) + { + var langs = (List) pc.UserData!; + + do { + var response = engine.ReadLine (cancellationToken); + + if (response == ".") + break; + + var tokens = response.Split (Space, 2); + if (tokens.Length != 2) + continue; + + langs.Add (new Pop3Language (tokens[0], tokens[1])); + } while (true); + } + + static Task ProcessLangResponse (Pop3Engine engine, Pop3Command pc, string text, bool doAsync, CancellationToken cancellationToken) + { + if (pc.Status != Pop3CommandStatus.Ok) + return Task.CompletedTask; + + if (doAsync) + return ReadLangResponseAsync (engine, pc, cancellationToken); + + ReadLangResponse (engine, pc, cancellationToken); + + return Task.CompletedTask; + } + + Pop3Command QueueLangCommand (out List langs) + { + CheckDisposed (); + CheckConnected (); + + if ((Capabilities & Pop3Capabilities.Lang) == 0) + throw new NotSupportedException ("The POP3 server does not support the LANG extension."); + + var pc = engine.QueueCommand (ProcessLangResponse, "LANG\r\n"); + pc.UserData = langs = new List (); + + return pc; } /// - /// Set the language used by the POP3 server for error messages. + /// Get the list of languages supported by the POP3 server. /// /// /// If the POP3 server supports the LANG extension, it is possible to - /// set the language used by the POP3 server for error messages. + /// query the list of languages supported by the POP3 server that can + /// be used for error messages. /// - /// The language code. + /// The supported languages. /// The cancellation token. - /// - /// is null. - /// - /// - /// is empty. - /// /// /// The has been disposed. /// @@ -1385,35 +1713,41 @@ void OnEngineDisconnected (object sender, EventArgs e) /// /// A POP3 protocol error occurred. /// - public void SetLanguage (string lang, CancellationToken cancellationToken = default (CancellationToken)) + public IList GetLanguages (CancellationToken cancellationToken = default) + { + var pc = QueueLangCommand (out var langs); + + engine.Run (true, cancellationToken); + + return new ReadOnlyCollection (langs); + } + + void CheckCanSetLanguage (string lang) { + CheckDisposed (); + CheckConnected (); + if (lang == null) throw new ArgumentNullException (nameof (lang)); if (lang.Length == 0) throw new ArgumentException ("The language code cannot be empty.", nameof (lang)); - CheckDisposed (); - CheckConnected (); - if ((Capabilities & Pop3Capabilities.Lang) == 0) throw new NotSupportedException ("The POP3 server does not support the LANG extension."); - - SendCommand (cancellationToken, "LANG {0}", lang); } /// - /// Asynchronously set the language used by the POP3 server for error messages. + /// Set the language used by the POP3 server for error messages. /// /// /// If the POP3 server supports the LANG extension, it is possible to /// set the language used by the POP3 server for error messages. /// - /// An asynchronous task context. /// The language code. /// The cancellation token. /// - /// is null. + /// is . /// /// /// is empty. @@ -1439,13 +1773,11 @@ void OnEngineDisconnected (object sender, EventArgs e) /// /// A POP3 protocol error occurred. /// - public Task SetLanguageAsync (string lang, CancellationToken cancellationToken = default (CancellationToken)) + public void SetLanguage (string lang, CancellationToken cancellationToken = default) { - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - SetLanguage (lang, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); + CheckCanSetLanguage (lang); + + SendCommand (cancellationToken, $"LANG {lang}\r\n"); } #region IMailSpool implementation @@ -1492,7 +1824,7 @@ public override int Count { /// along with and /// will fail. /// - /// true if supports UIDs; otherwise, false. + /// if supports UIDs; otherwise, . /// /// The has been disposed. /// @@ -1512,85 +1844,30 @@ public override bool SupportsUids { } } - /// - /// Get the number of messages available in the message spool. - /// - /// - /// Gets the number of messages available in the message spool. - /// - /// The number of available messages. - /// The cancellation token. - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The POP3 command failed. - /// - /// - /// A POP3 protocol error occurred. - /// - [Obsolete ("Use the Count property instead.")] - public override int GetMessageCount (CancellationToken cancellationToken = default (CancellationToken)) + static Task ProcessUidlResponse (Pop3Engine engine, Pop3Command pc, string text, bool doAsync, CancellationToken cancellationToken) { - CheckDisposed (); - CheckConnected (); - CheckAuthenticated (); + if (pc.Status != Pop3CommandStatus.Ok) + return Task.CompletedTask; + + var tokens = text.Split (Space, StringSplitOptions.RemoveEmptyEntries); + int seqid = GetExpectedSequenceId (pc); + + if (tokens.Length < 2) { + pc.Exception = CreatePop3ParseException ("Pop3 server returned an incomplete response to the UIDL command."); + return Task.CompletedTask; + } + + if (!int.TryParse (tokens[0], NumberStyles.None, CultureInfo.InvariantCulture, out int id) || id != seqid) { + pc.Exception = CreatePop3ParseException ("Pop3 server returned an unexpected response to the UIDL command."); + return Task.CompletedTask; + } + + pc.UserData = tokens[1]; - return total; + return Task.CompletedTask; } - /// - /// Get the UID of the message at the specified index. - /// - /// - /// Gets the UID of the message at the specified index. - /// Not all servers support UIDs, so you should first check the - /// property for the flag or - /// the convenience property. - /// - /// The message UID. - /// The message index. - /// The cancellation token. - /// - /// is not a valid message index. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The POP3 server does not support the UIDL extension. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The POP3 command failed. - /// - /// - /// A POP3 protocol error occurred. - /// - public override string GetMessageUid (int index, CancellationToken cancellationToken = default (CancellationToken)) + Pop3Command QueueUidlCommand (int index) { CheckDisposed (); CheckConnected (); @@ -1602,71 +1879,38 @@ public override bool SupportsUids { if (!SupportsUids && (probed & ProbedCapabilities.UIDL) != 0) throw new NotSupportedException ("The POP3 server does not support the UIDL extension."); - string uid = null; - - var pc = engine.QueueCommand (cancellationToken, (pop3, cmd, text) => { - if (cmd.Status != Pop3CommandStatus.Ok) - return; - - // the response should be " " - var tokens = text.Split (new [] { ' ' }, StringSplitOptions.RemoveEmptyEntries); - int seqid; - - if (tokens.Length < 2) { - cmd.Exception = CreatePop3ParseException ("Pop3 server returned an incomplete response to the UIDL command."); - return; - } - - if (!int.TryParse (tokens[0], out seqid) || seqid < 1) { - cmd.Exception = CreatePop3ParseException ("Pop3 server returned an unexpected response to the UIDL command."); - return; - } - - if (seqid != index + 1) { - cmd.Exception = CreatePop3ParseException ("Pop3 server returned the UID for the wrong message."); - return; - } - - uid = tokens[1]; - }, "UIDL {0}", index + 1); - - while (engine.Iterate () < pc.Id) { - // continue processing commands - } + return engine.QueueCommand (ProcessUidlResponse, "UIDL {0}\r\n", index + 1); + } + T OnUidlComplete (Pop3Command pc) + { probed |= ProbedCapabilities.UIDL; - if (pc.Status != Pop3CommandStatus.Ok) { - if (!SupportsUids) - throw new NotSupportedException ("The POP3 server does not support the UIDL extension."); - - throw CreatePop3Exception (pc); - } + if (pc.Status != Pop3CommandStatus.Ok && !SupportsUids) + throw new NotSupportedException ("The POP3 server does not support the UIDL extension."); - if (pc.Exception != null) - throw pc.Exception; + pc.ThrowIfError (); engine.Capabilities |= Pop3Capabilities.UIDL; - dict[uid] = index + 1; - - return uid; + return (T) pc.UserData!; } /// - /// Get the full list of available message UIDs. + /// Get the UID of the message at the specified index. /// /// - /// Gets the full list of available message UIDs. + /// Gets the UID of the message at the specified index. /// Not all servers support UIDs, so you should first check the /// property for the flag or /// the convenience property. /// - /// - /// - /// - /// The message uids. + /// The message UID. + /// The message index. /// The cancellation token. + /// + /// is not a valid message index. + /// /// /// The has been disposed. /// @@ -1682,212 +1926,100 @@ public override bool SupportsUids { /// /// The operation was canceled via the cancellation token. /// - /// - /// An I/O error occurred. - /// - /// - /// The POP3 command failed. - /// - /// - /// A POP3 protocol error occurred. - /// - public override IList GetMessageUids (CancellationToken cancellationToken = default (CancellationToken)) - { - CheckDisposed (); - CheckConnected (); - CheckAuthenticated (); - - if (!SupportsUids && (probed & ProbedCapabilities.UIDL) != 0) - throw new NotSupportedException ("The POP3 server does not support the UIDL extension."); - - dict.Clear (); - - var pc = engine.QueueCommand (cancellationToken, (pop3, cmd, text) => { - if (cmd.Status != Pop3CommandStatus.Ok) - return; - - do { - var response = engine.ReadLine (cmd.CancellationToken); - if (response == ".") - break; - - if (cmd.Exception != null) - continue; - - var tokens = response.Split (new [] { ' ' }, StringSplitOptions.RemoveEmptyEntries); - int seqid; - - if (tokens.Length < 2) { - cmd.Exception = CreatePop3ParseException ("Pop3 server returned an incomplete response to the UIDL command."); - continue; - } - - if (!int.TryParse (tokens[0], out seqid)) { - cmd.Exception = CreatePop3ParseException ("Pop3 server returned an invalid response to the UIDL command."); - continue; - } - - dict.Add (tokens[1], seqid); - } while (true); - }, "UIDL"); - - while (engine.Iterate () < pc.Id) { - // continue processing commands - } - - probed |= ProbedCapabilities.UIDL; - - if (pc.Status != Pop3CommandStatus.Ok) { - if (!SupportsUids) - throw new NotSupportedException ("The POP3 server does not support the UIDL extension."); - - throw CreatePop3Exception (pc); - } - - if (pc.Exception != null) - throw pc.Exception; + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override string GetMessageUid (int index, CancellationToken cancellationToken = default) + { + var pc = QueueUidlCommand (index); - engine.Capabilities |= Pop3Capabilities.UIDL; + engine.Run (false, cancellationToken); - return dict.Keys.ToArray (); + return OnUidlComplete (pc); } - class MessageSizeContext + static void ParseUidlAllResponse (Pop3Command pc, string response) { - protected readonly Pop3Engine Engine; - int[] sizes; - int index; - - public MessageSizeContext (Pop3Engine engine) - { - Engine = engine; - } - - void Reset (int capacity) - { - index = 0; - - if (sizes == null) { - sizes = new int[capacity]; - return; - } - - if (capacity != sizes.Length) - Array.Resize (ref sizes, capacity); - } - - void Add (int size) - { - sizes[index++] = size; - } - - void OnDataReceived (Pop3Engine pop3, Pop3Command pc, string text) - { - if (pc.Status != Pop3CommandStatus.Ok) - return; - - var tokens = text.Split (new [] { ' ' }, StringSplitOptions.RemoveEmptyEntries); - int id, size; - - if (tokens.Length < 2) { - pc.Exception = CreatePop3ParseException ("Pop3 server returned an incomplete response to the LIST command."); - return; - } + var tokens = response.Split (Space, StringSplitOptions.RemoveEmptyEntries); + var uids = (List) pc.UserData!; - if (!int.TryParse (tokens[0], out id) || id < 1) { - pc.Exception = CreatePop3ParseException ("Pop3 server returned an unexpected response to the LIST command."); - return; - } - - if (!int.TryParse (tokens[1], out size) || size < 0) { - pc.Exception = CreatePop3ParseException ("Pop3 server returned an unexpected size token to the LIST command."); - return; - } - - Add (size); + if (tokens.Length < 2) { + pc.Exception = CreatePop3ParseException ("Pop3 server returned an incomplete response to the UIDL command."); + return; } - Pop3Command QueueCommand (int seqid, CancellationToken cancellationToken) - { - return Engine.QueueCommand (cancellationToken, OnDataReceived, "LIST {0}", seqid); + if (!int.TryParse (tokens[0], NumberStyles.None, CultureInfo.InvariantCulture, out int seqid) || seqid != uids.Count + 1) { + pc.Exception = CreatePop3ParseException ("Pop3 server returned an invalid response to the UIDL command."); + return; } - void SendCommand (int seqid, CancellationToken cancellationToken) - { - var pc = QueueCommand (seqid, cancellationToken); + uids.Add (tokens[1]); + } - while (Engine.Iterate () < pc.Id) { - // continue processing commands - } + static void ReadUidlAllResponse (Pop3Engine engine, Pop3Command pc, CancellationToken cancellationToken) + { + do { + var response = engine.ReadLine (cancellationToken); - if (pc.Status != Pop3CommandStatus.Ok) - throw CreatePop3Exception (pc); + if (response == ".") + break; if (pc.Exception != null) - throw pc.Exception; - } + continue; - public int GetSize (int seqid, CancellationToken cancellationToken) - { - sizes = new int[1]; - index = 0; - - SendCommand (seqid, cancellationToken); - - return sizes[0]; - } - - public IList GetSizes (IList seqids, CancellationToken cancellationToken) - { - sizes = new int[seqids.Count]; - index = 0; - - if ((Engine.Capabilities & Pop3Capabilities.Pipelining) == 0) { - for (int i = 0; i < seqids.Count; i++) - SendCommand (seqids[i], cancellationToken); + ParseUidlAllResponse (pc, response); + } while (true); + } - return sizes; - } + static Task ProcessUidlAllResponse (Pop3Engine engine, Pop3Command pc, string text, bool doAsync, CancellationToken cancellationToken) + { + if (pc.Status != Pop3CommandStatus.Ok) + return Task.CompletedTask; - var commands = new Pop3Command[seqids.Count]; - Pop3Command pc = null; + if (doAsync) + return ReadUidlAllResponseAsync (engine, pc, cancellationToken); - for (int i = 0; i < seqids.Count; i++) - commands[i] = QueueCommand (seqids[i], cancellationToken); + ReadUidlAllResponse (engine, pc, cancellationToken); - pc = commands[commands.Length - 1]; + return Task.CompletedTask; + } - while (Engine.Iterate () < pc.Id) { - // continue processing commands - } + Pop3Command QueueUidlCommand () + { + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); - for (int i = 0; i < commands.Length; i++) { - if (commands[i].Status != Pop3CommandStatus.Ok) - throw CreatePop3Exception (commands[i]); + if (!SupportsUids && (probed & ProbedCapabilities.UIDL) != 0) + throw new NotSupportedException ("The POP3 server does not support the UIDL extension."); - if (commands[i].Exception != null) - throw commands[i].Exception; - } + var pc = engine.QueueCommand (ProcessUidlAllResponse, "UIDL\r\n"); + var uids = new List (); + pc.UserData = uids; - return sizes; - } + return pc; } /// - /// Get the size of the specified message, in bytes. + /// Get the full list of available message UIDs. /// /// - /// Gets the size of the specified message, in bytes. + /// Gets the full list of available message UIDs. + /// Not all servers support UIDs, so you should first check the + /// property for the flag or + /// the convenience property. /// - /// The message size, in bytes. - /// The UID of the message. + /// + /// + /// + /// The message uids. /// The cancellation token. - /// - /// is null. - /// - /// - /// is not a valid message UID. - /// /// /// The has been disposed. /// @@ -1897,6 +2029,9 @@ public IList GetSizes (IList seqids, CancellationToken cancellationTok /// /// The is not authenticated. /// + /// + /// The POP3 server does not support the UIDL extension. + /// /// /// The operation was canceled via the cancellation token. /// @@ -1909,26 +2044,53 @@ public IList GetSizes (IList seqids, CancellationToken cancellationTok /// /// A POP3 protocol error occurred. /// - [Obsolete ("Use GetMessageSize (int index, CancellationToken cancellationToken = default (CancellationToken)) instead.")] - public override int GetMessageSize (string uid, CancellationToken cancellationToken = default (CancellationToken)) + public override IList GetMessageUids (CancellationToken cancellationToken = default) + { + var pc = QueueUidlCommand (); + + engine.Run (false, cancellationToken); + + return OnUidlComplete> (pc); + } + + Task ProcessListResponse (Pop3Engine pop3, Pop3Command pc, string text, bool doAsync, CancellationToken cancellationToken) { - int seqid; + if (pc.Status != Pop3CommandStatus.Ok) + return Task.CompletedTask; + + var tokens = text.Split (Space, StringSplitOptions.RemoveEmptyEntries); + int seqid = GetExpectedSequenceId (pc); - if (uid == null) - throw new ArgumentNullException (nameof (uid)); + if (tokens.Length < 2) { + pc.Exception = CreatePop3ParseException ("Pop3 server returned an incomplete response to the LIST command: {0}", text); + return Task.CompletedTask; + } + + if (!int.TryParse (tokens[0], NumberStyles.None, CultureInfo.InvariantCulture, out int id) || id != seqid) { + pc.Exception = CreatePop3ParseException ("Pop3 server returned an unexpected sequence-id token to the LIST command: {0}", tokens[0]); + return Task.CompletedTask; + } + + if (!int.TryParse (tokens[1], NumberStyles.None, CultureInfo.InvariantCulture, out int size) || size < 0) { + pc.Exception = CreatePop3ParseException ("Pop3 server returned an unexpected size token to the LIST command: {0}", tokens[1]); + return Task.CompletedTask; + } + + pc.UserData = size; + + return Task.CompletedTask; + } + Pop3Command QueueListCommand (int index) + { CheckDisposed (); CheckConnected (); CheckAuthenticated (); - LoadUids (); - - if (!dict.TryGetValue (uid, out seqid)) - throw new ArgumentException ("No such message.", nameof (uid)); - - var ctx = new MessageSizeContext (engine); + if (index < 0 || index >= total) + throw new ArgumentOutOfRangeException (nameof (index)); - return ctx.GetSize (seqid, cancellationToken); + return engine.QueueCommand (ProcessListResponse, "LIST {0}\r\n", index + 1); } /// @@ -1964,18 +2126,77 @@ public IList GetSizes (IList seqids, CancellationToken cancellationTok /// /// A POP3 protocol error occurred. /// - public override int GetMessageSize (int index, CancellationToken cancellationToken = default (CancellationToken)) + public override int GetMessageSize (int index, CancellationToken cancellationToken = default) + { + var pc = QueueListCommand (index); + + engine.Run (true, cancellationToken); + + return (int) pc.UserData!; + } + + static void ParseListAllResponse (Pop3Command pc, string response) + { + var tokens = response.Split (Space, StringSplitOptions.RemoveEmptyEntries); + var sizes = (List) pc.UserData!; + + if (tokens.Length < 2) { + pc.Exception = CreatePop3ParseException ("Pop3 server returned an incomplete response to the LIST command: {0}", response); + return; + } + + if (!int.TryParse (tokens[0], NumberStyles.None, CultureInfo.InvariantCulture, out int seqid) || seqid != sizes.Count + 1) { + pc.Exception = CreatePop3ParseException ("Pop3 server returned an unexpected sequence-id token to the LIST command: {0}", tokens[0]); + return; + } + + if (!int.TryParse (tokens[1], NumberStyles.None, CultureInfo.InvariantCulture, out int size) || size < 0) { + pc.Exception = CreatePop3ParseException ("Pop3 server returned an unexpected size token to the LIST command: {0}", tokens[1]); + return; + } + + sizes.Add (size); + } + + static void ReadListAllResponse (Pop3Engine engine, Pop3Command pc, CancellationToken cancellationToken) + { + do { + var response = engine.ReadLine (cancellationToken); + + if (response == ".") + break; + + if (pc.Exception != null) + continue; + + ParseListAllResponse (pc, response); + } while (true); + } + + static Task ProcessListAllResponse (Pop3Engine engine, Pop3Command pc, string text, bool doAsync, CancellationToken cancellationToken) + { + if (pc.Status != Pop3CommandStatus.Ok) + return Task.CompletedTask; + + if (doAsync) + return ReadListAllResponseAsync (engine, pc, cancellationToken); + + ReadListAllResponse (engine, pc, cancellationToken); + + return Task.CompletedTask; + } + + List QueueListCommand () { CheckDisposed (); CheckConnected (); CheckAuthenticated (); - if (index < 0 || index >= total) - throw new ArgumentOutOfRangeException (nameof (index)); - - var ctx = new MessageSizeContext (engine); + var pc = engine.QueueCommand (ProcessListAllResponse, "LIST\r\n"); + var sizes = new List (); + pc.UserData = sizes; - return ctx.GetSize (index + 1, cancellationToken); + return sizes; } /// @@ -2007,203 +2228,226 @@ public IList GetSizes (IList seqids, CancellationToken cancellationTok /// /// A POP3 protocol error occurred. /// - public override IList GetMessageSizes (CancellationToken cancellationToken = default (CancellationToken)) + public override IList GetMessageSizes (CancellationToken cancellationToken = default) { - CheckDisposed (); - CheckConnected (); - CheckAuthenticated (); - - var sizes = new List (); - - var pc = engine.QueueCommand (cancellationToken, (pop3, cmd, text) => { - if (cmd.Status != Pop3CommandStatus.Ok) - return; - - do { - var response = engine.ReadLine (cmd.CancellationToken); - if (response == ".") - break; - - if (cmd.Exception != null) - continue; - - var tokens = response.Split (new [] { ' ' }, StringSplitOptions.RemoveEmptyEntries); - int seqid, size; - - if (tokens.Length < 2) { - cmd.Exception = CreatePop3ParseException ("Pop3 server returned an incomplete response to the LIST command."); - continue; - } - - if (!int.TryParse (tokens[0], out seqid) || seqid < 1) { - cmd.Exception = CreatePop3ParseException ("Pop3 server returned an unexpected response to the LIST command."); - continue; - } - - if (seqid != sizes.Count + 1) { - cmd.Exception = CreatePop3ParseException ("Pop3 server returned the size for the wrong message."); - continue; - } - - if (!int.TryParse (tokens[1], out size) || size < 0) { - cmd.Exception = CreatePop3ParseException ("Pop3 server returned an unexpected size token to the LIST command."); - continue; - } - - sizes.Add (size); - } while (true); - }, "LIST"); - - while (engine.Iterate () < pc.Id) { - // continue processing commands - } + var sizes = QueueListCommand (); - if (pc.Status != Pop3CommandStatus.Ok) - throw CreatePop3Exception (pc); - - if (pc.Exception != null) - throw pc.Exception; + engine.Run (true, cancellationToken); return sizes; } abstract class DownloadContext { - protected readonly Pop3Client Client; - readonly ITransferProgress Progress; - T[] downloaded; + readonly ITransferProgress? progress; + readonly Pop3Client client; + T[]? downloaded; long nread; - int index; + int idx; - protected DownloadContext (Pop3Client client, ITransferProgress progress) + protected DownloadContext (Pop3Client client, ITransferProgress? progress) { - Progress = progress; - Client = client; + this.progress = progress; + this.client = client; } protected Pop3Engine Engine { - get { return Client.engine; } + get { return client.engine; } } protected abstract T Parse (Pop3Stream data, CancellationToken cancellationToken); + protected abstract Task ParseAsync (Pop3Stream data, CancellationToken cancellationToken); + protected void Update (int n) { - if (Progress == null) + if (progress == null) return; nread += n; - Progress.Report (nread); + progress.Report (nread); } - void Reset (int capacity) + void OnDataReceived (Pop3Engine engine, Pop3Command pc, CancellationToken cancellationToken) { - nread = 0; - index = 0; + engine.CheckConnected (); - if (downloaded == null) { - downloaded = new T[capacity]; - return; - } + try { + engine.Stream.Mode = Pop3StreamMode.Data; - if (capacity != downloaded.Length) - Array.Resize (ref downloaded, capacity); - } + var item = Parse (engine.Stream, cancellationToken); - void Add (T item) - { - downloaded[index++] = item; + downloaded![idx++] = item; + } catch (FormatException ex) { + pc.Exception = CreatePop3ParseException (ex, "Failed to parse data."); + + engine.Stream.CopyTo (Stream.Null, 4096); + } finally { + engine.Stream.Mode = Pop3StreamMode.Line; + } } - void OnDataReceived (Pop3Engine pop3, Pop3Command pc, string text) + async Task OnDataReceivedAsync (Pop3Engine engine, Pop3Command pc, CancellationToken cancellationToken) { - if (pc.Status != Pop3CommandStatus.Ok) - return; + engine.CheckConnected (); try { - pop3.Stream.Mode = Pop3StreamMode.Data; - Add (Parse (pop3.Stream, pc.CancellationToken)); + engine.Stream.Mode = Pop3StreamMode.Data; + + var item = await ParseAsync (engine.Stream, cancellationToken).ConfigureAwait (false); + + downloaded![idx++] = item; } catch (FormatException ex) { pc.Exception = CreatePop3ParseException (ex, "Failed to parse data."); - pop3.Stream.CopyTo (Stream.Null, 4096); + + await engine.Stream.CopyToAsync (Stream.Null, 4096, cancellationToken).ConfigureAwait (false); } finally { - pop3.Stream.Mode = Pop3StreamMode.Line; + engine.Stream.Mode = Pop3StreamMode.Line; } } - Pop3Command QueueCommand (int seqid, bool headersOnly, CancellationToken cancellationToken) + Task OnDataReceived (Pop3Engine engine, Pop3Command pc, string text, bool doAsync, CancellationToken cancellationToken) + { + if (pc.Status != Pop3CommandStatus.Ok) + return Task.CompletedTask; + + if (doAsync) + return OnDataReceivedAsync (engine, pc, cancellationToken); + + OnDataReceived (engine, pc, cancellationToken); + + return Task.CompletedTask; + } + + Pop3Command QueueCommand (int index, bool headersOnly) { if (headersOnly) - return Engine.QueueCommand (cancellationToken, OnDataReceived, "TOP {0} 0", seqid); + return Engine.QueueCommand (OnDataReceived, "TOP {0} 0\r\n", index + 1); - return Engine.QueueCommand (cancellationToken, OnDataReceived, "RETR {0}", seqid); + return Engine.QueueCommand (OnDataReceived, "RETR {0}\r\n", index + 1); } - void DownloadItem (int seqid, bool headersOnly, CancellationToken cancellationToken) + void DownloadItem (int index, bool headersOnly, CancellationToken cancellationToken) { - var pc = QueueCommand (seqid, headersOnly, cancellationToken); + QueueCommand (index, headersOnly); - while (Engine.Iterate () < pc.Id) { - // continue processing commands - } + Engine.Run (true, cancellationToken); + } - if (pc.Status != Pop3CommandStatus.Ok) - throw CreatePop3Exception (pc); + async Task DownloadItemAsync (int index, bool headersOnly, CancellationToken cancellationToken) + { + QueueCommand (index, headersOnly); - if (pc.Exception != null) - throw pc.Exception; + await Engine.RunAsync (true, cancellationToken).ConfigureAwait (false); + } + + public T Download (int index, bool headersOnly, CancellationToken cancellationToken) + { + downloaded = new T[1]; + idx = 0; + + DownloadItem (index, headersOnly, cancellationToken); + + return downloaded[0]; } - public T Download (int seqid, bool headersOnly, CancellationToken cancellationToken) + public async Task DownloadAsync (int index, bool headersOnly, CancellationToken cancellationToken) { downloaded = new T[1]; - index = 0; + idx = 0; - DownloadItem (seqid, headersOnly, cancellationToken); + await DownloadItemAsync (index, headersOnly, cancellationToken).ConfigureAwait (false); return downloaded[0]; } - public IList Download (IList seqids, bool headersOnly, CancellationToken cancellationToken) + public IList Download (IList indexes, bool headersOnly, CancellationToken cancellationToken) + { + downloaded = new T[indexes.Count]; + idx = 0; + + if ((Engine.Capabilities & Pop3Capabilities.Pipelining) == 0) { + for (int i = 0; i < indexes.Count; i++) + DownloadItem (indexes[i], headersOnly, cancellationToken); + + return downloaded; + } + + for (int i = 0; i < indexes.Count; i++) + QueueCommand (indexes[i], headersOnly); + + Engine.Run (true, cancellationToken); + + return downloaded; + } + + public async Task> DownloadAsync (IList indexes, bool headersOnly, CancellationToken cancellationToken) { - downloaded = new T[seqids.Count]; - index = 0; + downloaded = new T[indexes.Count]; + idx = 0; if ((Engine.Capabilities & Pop3Capabilities.Pipelining) == 0) { - for (int i = 0; i < seqids.Count; i++) - DownloadItem (seqids[i], headersOnly, cancellationToken); + for (int i = 0; i < indexes.Count; i++) + await DownloadItemAsync (indexes[i], headersOnly, cancellationToken).ConfigureAwait (false); return downloaded; } - var commands = new Pop3Command[seqids.Count]; - Pop3Command pc = null; + for (int i = 0; i < indexes.Count; i++) + QueueCommand (indexes[i], headersOnly); + + await Engine.RunAsync (true, cancellationToken).ConfigureAwait (false); + + return downloaded; + } - for (int i = 0; i < seqids.Count; i++) - commands[i] = QueueCommand (seqids[i], headersOnly, cancellationToken); + public IList Download (int startIndex, int count, bool headersOnly, CancellationToken cancellationToken) + { + downloaded = new T[count]; + idx = 0; - pc = commands[commands.Length - 1]; + if ((Engine.Capabilities & Pop3Capabilities.Pipelining) == 0) { + for (int i = 0; i < count; i++) + DownloadItem (startIndex + i, headersOnly, cancellationToken); - while (Engine.Iterate () < pc.Id) { - // continue processing commands + return downloaded; } - for (int i = 0; i < commands.Length; i++) { - if (commands[i].Status != Pop3CommandStatus.Ok) - throw CreatePop3Exception (commands[i]); + for (int i = 0; i < count; i++) + QueueCommand (startIndex + i, headersOnly); + + Engine.Run (true, cancellationToken); + + return downloaded; + } + + public async Task> DownloadAsync (int startIndex, int count, bool headersOnly, CancellationToken cancellationToken) + { + downloaded = new T[count]; + idx = 0; + + if ((Engine.Capabilities & Pop3Capabilities.Pipelining) == 0) { + for (int i = 0; i < count; i++) + await DownloadItemAsync (startIndex + i, headersOnly, cancellationToken).ConfigureAwait (false); - if (commands[i].Exception != null) - throw commands[i].Exception; + return downloaded; } + for (int i = 0; i < count; i++) + QueueCommand (startIndex + i, headersOnly); + + await Engine.RunAsync (true, cancellationToken).ConfigureAwait (false); + return downloaded; } } class DownloadStreamContext : DownloadContext { - public DownloadStreamContext (Pop3Client client, ITransferProgress progress = null) : base (client, progress) + const int BufferSize = 4096; + + public DownloadStreamContext (Pop3Client client, ITransferProgress? progress = null) : base (client, progress) { } @@ -2211,18 +2455,52 @@ protected override Stream Parse (Pop3Stream data, CancellationToken cancellation { cancellationToken.ThrowIfCancellationRequested (); + var buffer = ArrayPool.Shared.Rent (BufferSize); var stream = new MemoryBlockStream (); - var buffer = new byte[4096]; - int nread; - while ((nread = data.Read (buffer, 0, buffer.Length, cancellationToken)) > 0) { - stream.Write (buffer, 0, nread); - Update (nread); + try { + int nread; + + while ((nread = data.Read (buffer, 0, BufferSize, cancellationToken)) > 0) { + stream.Write (buffer, 0, nread); + Update (nread); + } + + stream.Position = 0; + + return stream; + } catch { + stream.Dispose (); + throw; + } finally { + ArrayPool.Shared.Return (buffer); } + } + + protected override async Task ParseAsync (Pop3Stream data, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested (); + + var buffer = ArrayPool.Shared.Rent (BufferSize); + var stream = new MemoryBlockStream (); + + try { + int nread; + + while ((nread = await data.ReadAsync (buffer, 0, BufferSize, cancellationToken).ConfigureAwait (false)) > 0) { + stream.Write (buffer, 0, nread); + Update (nread); + } - stream.Position = 0; + stream.Position = 0; - return stream; + return stream; + } catch { + stream.Dispose (); + throw; + } finally { + ArrayPool.Shared.Return (buffer); + } } } @@ -2238,18 +2516,27 @@ public DownloadHeaderContext (Pop3Client client, MimeParser parser) : base (clie protected override HeaderList Parse (Pop3Stream data, CancellationToken cancellationToken) { using (var stream = new ProgressStream (data, Update)) { - parser.SetStream (ParserOptions.Default, stream); + parser.SetStream (stream); return parser.ParseMessage (cancellationToken).Headers; } } + + protected override async Task ParseAsync (Pop3Stream data, CancellationToken cancellationToken) + { + using (var stream = new ProgressStream (data, Update)) { + parser.SetStream (stream); + + return (await parser.ParseMessageAsync (cancellationToken).ConfigureAwait (false)).Headers; + } + } } class DownloadMessageContext : DownloadContext { readonly MimeParser parser; - public DownloadMessageContext (Pop3Client client, MimeParser parser, ITransferProgress progress = null) : base (client, progress) + public DownloadMessageContext (Pop3Client client, MimeParser parser, ITransferProgress? progress = null) : base (client, progress) { this.parser = parser; } @@ -2257,137 +2544,78 @@ public DownloadMessageContext (Pop3Client client, MimeParser parser, ITransferPr protected override MimeMessage Parse (Pop3Stream data, CancellationToken cancellationToken) { using (var stream = new ProgressStream (data, Update)) { - parser.SetStream (ParserOptions.Default, stream); + parser.SetStream (stream); return parser.ParseMessage (cancellationToken); } } + + protected override Task ParseAsync (Pop3Stream data, CancellationToken cancellationToken) + { + using (var stream = new ProgressStream (data, Update)) { + parser.SetStream (stream); + + return parser.ParseMessageAsync (cancellationToken); + } + } } - /// - /// Get the headers for the specified message. - /// - /// - /// Gets the headers for the specified message. - /// - /// The message headers. - /// The UID of the message. - /// The cancellation token. - /// - /// is null. - /// - /// - /// is not a valid message UID. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The POP3 server does not support the UIDL extension. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The POP3 command failed. - /// - /// - /// A POP3 protocol error occurred. - /// - [Obsolete ("Use GetMessageHeaders (int index, CancellationToken cancellationToken = default (CancellationToken)) instead.")] - public override HeaderList GetMessageHeaders (string uid, CancellationToken cancellationToken = default (CancellationToken)) + void CheckCanDownload (int index) { - int seqid; + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); - if (uid == null) - throw new ArgumentNullException (nameof (uid)); + if (index < 0 || index >= total) + throw new ArgumentOutOfRangeException (nameof (index)); + } + bool CheckCanDownload (IList indexes) + { CheckDisposed (); CheckConnected (); CheckAuthenticated (); - LoadUids (); + if (indexes == null) + throw new ArgumentNullException (nameof (indexes)); - if (!dict.TryGetValue (uid, out seqid)) - throw new ArgumentException ("No such message.", nameof (uid)); + if (indexes.Count == 0) + return false; - var ctx = new DownloadHeaderContext (this, parser); + for (int i = 0; i < indexes.Count; i++) { + if (indexes[i] < 0 || indexes[i] >= total) + throw new ArgumentException ("One or more of the indexes are invalid.", nameof (indexes)); + } - return ctx.Download (seqid, true, cancellationToken); + return true; } - /// - /// Get the headers for the message at the specified index. - /// - /// - /// Gets the headers for the message at the specified index. - /// - /// The message headers. - /// The index of the message. - /// The cancellation token. - /// - /// is not a valid message index. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The POP3 command failed. - /// - /// - /// A POP3 protocol error occurred. - /// - public override HeaderList GetMessageHeaders (int index, CancellationToken cancellationToken = default (CancellationToken)) + bool CheckCanDownload (int startIndex, int count) { CheckDisposed (); CheckConnected (); CheckAuthenticated (); - if (index < 0 || index >= total) - throw new ArgumentOutOfRangeException (nameof (index)); + if (startIndex < 0 || startIndex >= total) + throw new ArgumentOutOfRangeException (nameof (startIndex)); - var ctx = new DownloadHeaderContext (this, parser); + if (count < 0 || count > (total - startIndex)) + throw new ArgumentOutOfRangeException (nameof (count)); - return ctx.Download (index + 1, true, cancellationToken); + return count > 0; } /// - /// Get the headers for the specified messages. + /// Get the headers for the message at the specified index. /// /// - /// Gets the headers for the specified messages. + /// Gets the headers for the message at the specified index. /// - /// The headers for the specified messages. - /// The UIDs of the messages. + /// The message headers. + /// The index of the message. /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the are invalid. - /// -or- - /// No uids were specified. + /// + /// is not a valid message index. /// /// /// The has been disposed. @@ -2398,9 +2626,6 @@ protected override MimeMessage Parse (Pop3Stream data, CancellationToken cancell /// /// The is not authenticated. /// - /// - /// The POP3 server does not support the UIDL extension. - /// /// /// The operation was canceled via the cancellation token. /// @@ -2413,35 +2638,13 @@ protected override MimeMessage Parse (Pop3Stream data, CancellationToken cancell /// /// A POP3 protocol error occurred. /// - [Obsolete ("Use GetMessageHeaders (IList indexes, CancellationToken cancellationToken = default (CancellationToken)) instead.")] - public override IList GetMessageHeaders (IList uids, CancellationToken cancellationToken = default (CancellationToken)) + public override HeaderList GetMessageHeaders (int index, CancellationToken cancellationToken = default) { - if (uids == null) - throw new ArgumentNullException (nameof (uids)); - - if (uids.Count == 0) - throw new ArgumentException ("No uids specified.", nameof (uids)); - - CheckDisposed (); - CheckConnected (); - CheckAuthenticated (); - - LoadUids (); - - var seqids = new int[uids.Count]; - - for (int i = 0; i < uids.Count; i++) { - int seqid; - - if (!dict.TryGetValue (uids[i], out seqid)) - throw new ArgumentException ("One or more of the uids is invalid.", nameof (uids)); - - seqids[i] = seqid; - } + CheckCanDownload (index); var ctx = new DownloadHeaderContext (this, parser); - return ctx.Download (seqids, true, cancellationToken); + return ctx.Download (index, true, cancellationToken); } /// @@ -2458,12 +2661,10 @@ protected override MimeMessage Parse (Pop3Stream data, CancellationToken cancell /// The indexes of the messages. /// The cancellation token. /// - /// is null. + /// is . /// /// - /// One or more of the are invalid. - /// -or- - /// No indexes were specified. + /// One or more of the are invalid. /// /// /// The has been disposed. @@ -2489,30 +2690,14 @@ protected override MimeMessage Parse (Pop3Stream data, CancellationToken cancell /// /// A POP3 protocol error occurred. /// - public override IList GetMessageHeaders (IList indexes, CancellationToken cancellationToken = default (CancellationToken)) + public override IList GetMessageHeaders (IList indexes, CancellationToken cancellationToken = default) { - if (indexes == null) - throw new ArgumentNullException (nameof (indexes)); - - if (indexes.Count == 0) - throw new ArgumentException ("No indexes specified.", nameof (indexes)); - - CheckDisposed (); - CheckConnected (); - CheckAuthenticated (); - - var seqids = new int[indexes.Count]; - - for (int i = 0; i < indexes.Count; i++) { - if (indexes[i] < 0 || indexes[i] >= total) - throw new ArgumentException ("One or more of the indexes are invalid.", nameof (indexes)); - - seqids[i] = indexes[i] + 1; - } + if (!CheckCanDownload (indexes)) + return Array.Empty (); var ctx = new DownloadHeaderContext (this, parser); - return ctx.Download (seqids, true, cancellationToken); + return ctx.Download (indexes, true, cancellationToken); } /// @@ -2556,160 +2741,32 @@ protected override MimeMessage Parse (Pop3Stream data, CancellationToken cancell /// /// /// A POP3 protocol error occurred. - /// - public override IList GetMessageHeaders (int startIndex, int count, CancellationToken cancellationToken = default (CancellationToken)) - { - if (startIndex < 0 || startIndex >= total) - throw new ArgumentOutOfRangeException (nameof (startIndex)); - - if (count < 0 || count > (total - startIndex)) - throw new ArgumentOutOfRangeException (nameof (count)); - - CheckDisposed (); - CheckConnected (); - CheckAuthenticated (); - - if (count == 0) - return new HeaderList[0]; - - var seqids = new int[count]; - - for (int i = 0; i < count; i++) - seqids[i] = startIndex + i + 1; - - var ctx = new DownloadHeaderContext (this, parser); - - return ctx.Download (seqids, true, cancellationToken); - } - - /// - /// Get the message with the specified UID. - /// - /// - /// Gets the message with the specified UID. - /// - /// The message. - /// The UID of the message. - /// The cancellation token. - /// - /// is null. - /// - /// - /// is not a valid message UID. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The POP3 server does not support the UIDL extension. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The POP3 command failed. - /// - /// - /// A POP3 protocol error occurred. - /// - [Obsolete ("Use GetMessage (int index, CancellationToken cancellationToken = default (CancellationToken)) instead.")] - public override MimeMessage GetMessage (string uid, CancellationToken cancellationToken = default (CancellationToken)) - { - int seqid; - - if (uid == null) - throw new ArgumentNullException (nameof (uid)); - - CheckDisposed (); - CheckConnected (); - CheckAuthenticated (); - - LoadUids (); - - if (!dict.TryGetValue (uid, out seqid)) - throw new ArgumentException ("No such message.", nameof (uid)); - - var ctx = new DownloadMessageContext (this, parser); - - return ctx.Download (seqid, false, cancellationToken); - } - - /// - /// Get the message at the specified index. - /// - /// - /// Gets the message at the specified index. - /// - /// - /// - /// - /// The message. - /// The index of the message. - /// The cancellation token. - /// The progress reporting mechanism. - /// - /// is not a valid message index. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The POP3 command failed. - /// - /// - /// A POP3 protocol error occurred. - /// - public override MimeMessage GetMessage (int index, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) - { - CheckDisposed (); - CheckConnected (); - CheckAuthenticated (); - - if (index < 0 || index >= total) - throw new ArgumentOutOfRangeException (nameof (index)); + /// + public override IList GetMessageHeaders (int startIndex, int count, CancellationToken cancellationToken = default) + { + if (!CheckCanDownload (startIndex, count)) + return Array.Empty (); - var ctx = new DownloadMessageContext (this, parser, progress); + var ctx = new DownloadHeaderContext (this, parser); - return ctx.Download (index + 1, false, cancellationToken); + return ctx.Download (startIndex, count, true, cancellationToken); } /// - /// Get the messages with the specified UIDs. + /// Get the message at the specified index. /// /// - /// Gets the messages with the specified UIDs. + /// Gets the message at the specified index. /// - /// The messages. - /// The UIDs of the messages. + /// + /// + /// + /// The message. + /// The index of the message. /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the are invalid. - /// -or- - /// No uids were specified. + /// The progress reporting mechanism. + /// + /// is not a valid message index. /// /// /// The has been disposed. @@ -2720,9 +2777,6 @@ protected override MimeMessage Parse (Pop3Stream data, CancellationToken cancell /// /// The is not authenticated. /// - /// - /// The POP3 server does not support the UIDL extension. - /// /// /// The operation was canceled via the cancellation token. /// @@ -2735,35 +2789,13 @@ protected override MimeMessage Parse (Pop3Stream data, CancellationToken cancell /// /// A POP3 protocol error occurred. /// - [Obsolete ("Use GetMessages (IList indexes, CancellationToken cancellationToken = default (CancellationToken)) instead.")] - public override IList GetMessages (IList uids, CancellationToken cancellationToken = default (CancellationToken)) + public override MimeMessage GetMessage (int index, CancellationToken cancellationToken = default, ITransferProgress? progress = null) { - if (uids == null) - throw new ArgumentNullException (nameof (uids)); - - if (uids.Count == 0) - throw new ArgumentException ("No uids specified.", nameof (uids)); - - CheckDisposed (); - CheckConnected (); - CheckAuthenticated (); - - LoadUids (); - - var seqids = new int[uids.Count]; - - for (int i = 0; i < uids.Count; i++) { - int seqid; - - if (!dict.TryGetValue (uids[i], out seqid)) - throw new ArgumentException ("One or more of the uids is invalid.", nameof (uids)); - - seqids[i] = seqid; - } + CheckCanDownload (index); - var ctx = new DownloadMessageContext (this, parser); + var ctx = new DownloadMessageContext (this, parser, progress); - return ctx.Download (seqids, false, cancellationToken); + return ctx.Download (index, false, cancellationToken); } /// @@ -2781,12 +2813,10 @@ protected override MimeMessage Parse (Pop3Stream data, CancellationToken cancell /// The cancellation token. /// The progress reporting mechanism. /// - /// is null. + /// is . /// /// - /// One or more of the are invalid. - /// -or- - /// No indexes were specified. + /// One or more of the are invalid. /// /// /// The has been disposed. @@ -2812,30 +2842,14 @@ protected override MimeMessage Parse (Pop3Stream data, CancellationToken cancell /// /// A POP3 protocol error occurred. /// - public override IList GetMessages (IList indexes, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + public override IList GetMessages (IList indexes, CancellationToken cancellationToken = default, ITransferProgress? progress = null) { - if (indexes == null) - throw new ArgumentNullException (nameof (indexes)); - - if (indexes.Count == 0) - throw new ArgumentException ("No indexes specified.", nameof (indexes)); - - CheckDisposed (); - CheckConnected (); - CheckAuthenticated (); - - var seqids = new int[indexes.Count]; - - for (int i = 0; i < indexes.Count; i++) { - if (indexes[i] < 0 || indexes[i] >= total) - throw new ArgumentException ("One or more of the indexes are invalid.", nameof (indexes)); - - seqids[i] = indexes[i] + 1; - } + if (!CheckCanDownload (indexes)) + return Array.Empty (); var ctx = new DownloadMessageContext (this, parser, progress); - return ctx.Download (seqids, false, cancellationToken); + return ctx.Download (indexes, false, cancellationToken); } /// @@ -2884,29 +2898,14 @@ protected override MimeMessage Parse (Pop3Stream data, CancellationToken cancell /// /// A POP3 protocol error occurred. /// - public override IList GetMessages (int startIndex, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + public override IList GetMessages (int startIndex, int count, CancellationToken cancellationToken = default, ITransferProgress? progress = null) { - if (startIndex < 0 || startIndex >= total) - throw new ArgumentOutOfRangeException (nameof (startIndex)); - - if (count < 0 || count > (total - startIndex)) - throw new ArgumentOutOfRangeException (nameof (count)); - - CheckDisposed (); - CheckConnected (); - CheckAuthenticated (); - - if (count == 0) - return new MimeMessage[0]; - - var seqids = new int[count]; - - for (int i = 0; i < count; i++) - seqids[i] = startIndex + i + 1; + if (!CheckCanDownload (startIndex, count)) + return Array.Empty (); var ctx = new DownloadMessageContext (this, parser, progress); - return ctx.Download (seqids, false, cancellationToken); + return ctx.Download (startIndex, count, false, cancellationToken); } /// @@ -2917,7 +2916,7 @@ protected override MimeMessage Parse (Pop3Stream data, CancellationToken cancell /// /// The message or header stream. /// The index of the message. - /// true if only the headers should be retrieved; otherwise, false. + /// if only the headers should be retrieved; otherwise, . /// The cancellation token. /// The progress reporting mechanism. /// @@ -2944,18 +2943,13 @@ protected override MimeMessage Parse (Pop3Stream data, CancellationToken cancell /// /// A POP3 protocol error occurred. /// - public override Stream GetStream (int index, bool headersOnly = false, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + public override Stream GetStream (int index, bool headersOnly = false, CancellationToken cancellationToken = default, ITransferProgress? progress = null) { - CheckDisposed (); - CheckConnected (); - CheckAuthenticated (); - - if (index < 0 || index >= total) - throw new ArgumentOutOfRangeException (nameof (index)); + CheckCanDownload (index); var ctx = new DownloadStreamContext (this, progress); - return ctx.Download (index + 1, headersOnly, cancellationToken); + return ctx.Download (index, headersOnly, cancellationToken); } /// @@ -2970,16 +2964,14 @@ protected override MimeMessage Parse (Pop3Stream data, CancellationToken cancell /// /// The message or header streams. /// The indexes of the messages. - /// true if only the headers should be retrieved; otherwise, false. + /// if only the headers should be retrieved; otherwise, . /// The cancellation token. /// The progress reporting mechanism. /// - /// is null. + /// is . /// /// - /// One or more of the are invalid. - /// -or- - /// No indexes were specified. + /// One or more of the are invalid. /// /// /// The has been disposed. @@ -3005,30 +2997,14 @@ protected override MimeMessage Parse (Pop3Stream data, CancellationToken cancell /// /// A POP3 protocol error occurred. /// - public override IList GetStreams (IList indexes, bool headersOnly = false, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + public override IList GetStreams (IList indexes, bool headersOnly = false, CancellationToken cancellationToken = default, ITransferProgress? progress = null) { - CheckDisposed (); - CheckConnected (); - CheckAuthenticated (); - - if (indexes == null) - throw new ArgumentNullException (nameof (indexes)); - - if (indexes.Count == 0) - throw new ArgumentException ("No indexes specified.", nameof (indexes)); - - var seqids = new int[indexes.Count]; - - for (int i = 0; i < indexes.Count; i++) { - if (indexes[i] < 0 || indexes[i] >= total) - throw new ArgumentException ("One or more of the indexes are invalid.", nameof (indexes)); - - seqids[i] = indexes[i] + 1; - } + if (!CheckCanDownload (indexes)) + return Array.Empty (); var ctx = new DownloadStreamContext (this, progress); - return ctx.Download (seqids, headersOnly, cancellationToken); + return ctx.Download (indexes, headersOnly, cancellationToken); } /// @@ -3044,7 +3020,7 @@ protected override MimeMessage Parse (Pop3Stream data, CancellationToken cancell /// The message or header streams. /// The index of the first stream to get. /// The number of streams to get. - /// true if only the headers should be retrieved; otherwise, false. + /// if only the headers should be retrieved; otherwise, . /// The cancellation token. /// The progress reporting mechanism. /// @@ -3075,89 +3051,26 @@ protected override MimeMessage Parse (Pop3Stream data, CancellationToken cancell /// /// A POP3 protocol error occurred. /// - public override IList GetStreams (int startIndex, int count, bool headersOnly = false, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + public override IList GetStreams (int startIndex, int count, bool headersOnly = false, CancellationToken cancellationToken = default, ITransferProgress? progress = null) { - CheckDisposed (); - CheckConnected (); - CheckAuthenticated (); - - if (startIndex < 0 || startIndex >= total) - throw new ArgumentOutOfRangeException (nameof (startIndex)); - - if (count < 0 || count > (total - startIndex)) - throw new ArgumentOutOfRangeException (nameof (count)); - - if (count == 0) - return new Stream[0]; - - var seqids = new int[count]; - - for (int i = 0; i < count; i++) - seqids[i] = startIndex + i + 1; + if (!CheckCanDownload (startIndex, count)) + return Array.Empty (); var ctx = new DownloadStreamContext (this, progress); - return ctx.Download (seqids, headersOnly, cancellationToken); + return ctx.Download (startIndex, count, headersOnly, cancellationToken); } - /// - /// Mark the specified message for deletion. - /// - /// - /// Messages marked for deletion are not actually deleted until the session - /// is cleanly disconnected - /// (see ). - /// - /// The UID of the message. - /// The cancellation token. - /// - /// is null. - /// - /// - /// is not a valid message UID. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The POP3 server does not support the UIDL extension. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The POP3 command failed. - /// - /// - /// A POP3 protocol error occurred. - /// - [Obsolete ("Use DeleteMessage (int index, CancellationToken cancellationToken = default (CancellationToken)) instead.")] - public override void DeleteMessage (string uid, CancellationToken cancellationToken = default (CancellationToken)) + void CheckCanDelete (int index, out string seqid) { - int seqid; - - if (uid == null) - throw new ArgumentNullException (nameof (uid)); - CheckDisposed (); CheckConnected (); CheckAuthenticated (); - LoadUids (); - - if (!dict.TryGetValue (uid, out seqid)) - throw new ArgumentException ("No such message.", nameof (uid)); + if (index < 0 || index >= total) + throw new ArgumentOutOfRangeException (nameof (index)); - SendCommand (cancellationToken, "DELE {0}", seqid); + seqid = (index + 1).ToString (CultureInfo.InvariantCulture); } /// @@ -3197,109 +3110,31 @@ protected override MimeMessage Parse (Pop3Stream data, CancellationToken cancell /// /// A POP3 protocol error occurred. /// - public override void DeleteMessage (int index, CancellationToken cancellationToken = default (CancellationToken)) + public override void DeleteMessage (int index, CancellationToken cancellationToken = default) { - CheckDisposed (); - CheckConnected (); - CheckAuthenticated (); - - if (index < 0 || index >= total) - throw new ArgumentOutOfRangeException (nameof (index)); + CheckCanDelete (index, out string seqid); - SendCommand (cancellationToken, "DELE {0}", index + 1); + SendCommand (cancellationToken, $"DELE {seqid}\r\n"); } - /// - /// Mark the specified messages for deletion. - /// - /// - /// Messages marked for deletion are not actually deleted until the session - /// is cleanly disconnected - /// (see ). - /// - /// The UIDs of the messages. - /// The cancellation token. - /// - /// is null. - /// - /// - /// One or more of the are invalid. - /// -or- - /// No uids were specified. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// The is not authenticated. - /// - /// - /// The POP3 server does not support the UIDL extension. - /// - /// - /// The operation was canceled via the cancellation token. - /// - /// - /// An I/O error occurred. - /// - /// - /// The POP3 command failed. - /// - /// - /// A POP3 protocol error occurred. - /// - [Obsolete ("Use DeleteMessages (IList indexes, CancellationToken cancellationToken = default (CancellationToken)) instead.")] - public override void DeleteMessages (IList uids, CancellationToken cancellationToken = default (CancellationToken)) + bool CheckCanDelete (IList indexes) { - if (uids == null) - throw new ArgumentNullException (nameof (uids)); - - if (uids.Count == 0) - throw new ArgumentException ("No uids specified.", nameof (uids)); - CheckDisposed (); CheckConnected (); CheckAuthenticated (); - LoadUids (); - - var seqids = new int[uids.Count]; - - for (int i = 0; i < uids.Count; i++) { - int seqid; - - if (!dict.TryGetValue (uids[i], out seqid)) - throw new ArgumentException ("One or more of the uids are invalid.", nameof (uids)); - - seqids[i] = seqid; - } - - if ((Capabilities & Pop3Capabilities.Pipelining) == 0) { - for (int i = 0; i < seqids.Length; i++) - SendCommand (cancellationToken, "DELE {0}", seqids[i]); - - return; - } - - var commands = new Pop3Command[seqids.Length]; - Pop3Command pc = null; + if (indexes == null) + throw new ArgumentNullException (nameof (indexes)); - for (int i = 0; i < seqids.Length; i++) { - pc = engine.QueueCommand (cancellationToken, null, "DELE {0}", seqids[i]); - commands[i] = pc; - } + if (indexes.Count == 0) + return false; - while (engine.Iterate () < pc.Id) { - // continue processing commands + for (int i = 0; i < indexes.Count; i++) { + if (indexes[i] < 0 || indexes[i] >= total) + throw new ArgumentException ("One or more of the indexes are invalid.", nameof (indexes)); } - for (int i = 0; i < commands.Length; i++) { - if (commands[i].Status != Pop3CommandStatus.Ok) - throw CreatePop3Exception (commands[i]); - } + return true; } /// @@ -3313,12 +3148,10 @@ protected override MimeMessage Parse (Pop3Stream data, CancellationToken cancell /// The indexes of the messages. /// The cancellation token. /// - /// is null. + /// is . /// /// - /// One or more of the are invalid. - /// -or- - /// No indexes were specified. + /// One or more of the are invalid. /// /// /// The has been disposed. @@ -3341,50 +3174,37 @@ protected override MimeMessage Parse (Pop3Stream data, CancellationToken cancell /// /// A POP3 protocol error occurred. /// - public override void DeleteMessages (IList indexes, CancellationToken cancellationToken = default (CancellationToken)) + public override void DeleteMessages (IList indexes, CancellationToken cancellationToken = default) { - if (indexes == null) - throw new ArgumentNullException (nameof (indexes)); - - if (indexes.Count == 0) - throw new ArgumentException ("No indexes specified.", nameof (indexes)); - - CheckDisposed (); - CheckConnected (); - CheckAuthenticated (); - - var seqids = new int[indexes.Count]; - - for (int i = 0; i < indexes.Count; i++) { - if (indexes[i] < 0 || indexes[i] >= total) - throw new ArgumentException ("One or more of the indexes are invalid.", nameof (indexes)); - - seqids[i] = indexes[i] + 1; - } + if (!CheckCanDelete (indexes)) + return; if ((Capabilities & Pop3Capabilities.Pipelining) == 0) { - for (int i = 0; i < seqids.Length; i++) - SendCommand (cancellationToken, "DELE {0}", seqids[i]); + for (int i = 0; i < indexes.Count; i++) + SendCommand (cancellationToken, "DELE {0}\r\n", indexes[i] + 1); return; } - var commands = new Pop3Command[seqids.Length]; - Pop3Command pc = null; + for (int i = 0; i < indexes.Count; i++) + engine.QueueCommand (null, "DELE {0}\r\n", indexes[i] + 1); - for (int i = 0; i < seqids.Length; i++) { - pc = engine.QueueCommand (cancellationToken, null, "DELE {0}", seqids[i]); - commands[i] = pc; - } + engine.Run (true, cancellationToken); + } - while (engine.Iterate () < pc.Id) { - // continue processing commands - } + bool CheckCanDelete (int startIndex, int count) + { + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); - for (int i = 0; i < commands.Length; i++) { - if (commands[i].Status != Pop3CommandStatus.Ok) - throw CreatePop3Exception (commands[i]); - } + if (startIndex < 0 || startIndex >= total) + throw new ArgumentOutOfRangeException (nameof (startIndex)); + + if (count < 0 || count > (total - startIndex)) + throw new ArgumentOutOfRangeException (nameof (count)); + + return count >= 0; } /// @@ -3426,44 +3246,22 @@ protected override MimeMessage Parse (Pop3Stream data, CancellationToken cancell /// /// A POP3 protocol error occurred. /// - public override void DeleteMessages (int startIndex, int count, CancellationToken cancellationToken = default (CancellationToken)) + public override void DeleteMessages (int startIndex, int count, CancellationToken cancellationToken = default) { - if (startIndex < 0 || startIndex >= total) - throw new ArgumentOutOfRangeException (nameof (startIndex)); - - if (count < 0 || count > (total - startIndex)) - throw new ArgumentOutOfRangeException (nameof (count)); - - CheckDisposed (); - CheckConnected (); - CheckAuthenticated (); - - if (count == 0) + if (!CheckCanDelete (startIndex, count)) return; if ((Capabilities & Pop3Capabilities.Pipelining) == 0) { for (int i = 0; i < count; i++) - SendCommand (cancellationToken, "DELE {0}", startIndex + i + 1); + SendCommand (cancellationToken, "DELE {0}\r\n", startIndex + i + 1); return; } - var commands = new Pop3Command[count]; - Pop3Command pc = null; - - for (int i = 0; i < count; i++) { - pc = engine.QueueCommand (cancellationToken, null, "DELE {0}", startIndex + i + 1); - commands[i] = pc; - } - - while (engine.Iterate () < pc.Id) { - // continue processing commands - } + for (int i = 0; i < count; i++) + engine.QueueCommand (null, "DELE {0}\r\n", startIndex + i + 1); - for (int i = 0; i < commands.Length; i++) { - if (commands[i].Status != Pop3CommandStatus.Ok) - throw CreatePop3Exception (commands[i]); - } + engine.Run (true, cancellationToken); } /// @@ -3496,7 +3294,7 @@ protected override MimeMessage Parse (Pop3Stream data, CancellationToken cancell /// /// A POP3 protocol error occurred. /// - public override void DeleteAllMessages (CancellationToken cancellationToken = default (CancellationToken)) + public override void DeleteAllMessages (CancellationToken cancellationToken = default) { if (total > 0) DeleteMessages (0, total, cancellationToken); @@ -3532,13 +3330,13 @@ protected override MimeMessage Parse (Pop3Stream data, CancellationToken cancell /// /// A POP3 protocol error occurred. /// - public override void Reset (CancellationToken cancellationToken = default (CancellationToken)) + public override void Reset (CancellationToken cancellationToken = default) { CheckDisposed (); CheckConnected (); CheckAuthenticated (); - SendCommand (cancellationToken, "RSET"); + SendCommand (cancellationToken, "RSET\r\n"); } #endregion @@ -3546,7 +3344,7 @@ protected override MimeMessage Parse (Pop3Stream data, CancellationToken cancell #region IEnumerable implementation /// - /// Gets an enumerator for the messages in the folder. + /// Get an enumerator for the messages in the folder. /// /// /// Gets an enumerator for the messages in the folder. @@ -3595,18 +3393,12 @@ public override IEnumerator GetEnumerator () /// Releases the unmanaged resources used by the and /// optionally releases the managed resources. /// - /// true to release both managed and unmanaged resources; - /// false to release only the unmanaged resources. + /// to release both managed and unmanaged resources; + /// to release only the unmanaged resources. protected override void Dispose (bool disposing) { if (disposing && !disposed) { - engine.Disconnect (); - -#if NETFX_CORE - if (socket != null) - socket.Dispose (); -#endif - + engine.Disconnect (null); disposed = true; } diff --git a/MailKit/Net/Pop3/Pop3Command.cs b/MailKit/Net/Pop3/Pop3Command.cs index a6d927e04a..7c13254805 100644 --- a/MailKit/Net/Pop3/Pop3Command.cs +++ b/MailKit/Net/Pop3/Pop3Command.cs @@ -1,9 +1,9 @@ -// +// // Pop3Command.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -27,10 +27,8 @@ using System; using System.Text; using System.Threading; - -#if NETFX_CORE -using Encoding = Portable.Text.Encoding; -#endif +using System.Globalization; +using System.Threading.Tasks; namespace MailKit.Net.Pop3 { /// @@ -41,7 +39,7 @@ namespace MailKit.Net.Pop3 { /// force-disconnect the connection. If a non-fatal error occurs, set /// it on the property. /// - delegate void Pop3CommandHandler (Pop3Engine engine, Pop3Command pc, string text); + delegate Task Pop3CommandHandler (Pop3Engine engine, Pop3Command pc, string text, bool doAsync, CancellationToken cancellationToken); enum Pop3CommandStatus { Queued = -5, @@ -54,23 +52,42 @@ enum Pop3CommandStatus { class Pop3Command { - public CancellationToken CancellationToken { get; private set; } - public Pop3CommandHandler Handler { get; private set; } + public Pop3CommandHandler? Handler { get; private set; } public Encoding Encoding { get; private set; } public string Command { get; private set; } - public int Id { get; internal set; } // output public Pop3CommandStatus Status { get; internal set; } - public ProtocolException Exception { get; set; } - public string StatusText { get; set; } + public ProtocolException? Exception { get; set; } + public string? StatusText { get; set; } + + public object? UserData { get; set; } - public Pop3Command (CancellationToken cancellationToken, Pop3CommandHandler handler, Encoding encoding, string format, params object[] args) + public Pop3Command (Pop3CommandHandler? handler, Encoding encoding, string format, params object[] args) { - Command = string.Format (format, args); - CancellationToken = cancellationToken; + Command = string.Format (CultureInfo.InvariantCulture, format, args); Encoding = encoding; Handler = handler; } + + static Exception CreatePop3Exception (Pop3Command pc) + { + var command = pc.Command.Split (' ')[0].TrimEnd (); + var message = string.Format ("POP3 server did not respond with a +OK response to the {0} command.", command); + + if (pc.Status == Pop3CommandStatus.Error) + return new Pop3CommandException (message, pc.StatusText!); + + return new Pop3ProtocolException (message); + } + + public void ThrowIfError () + { + if (Status != Pop3CommandStatus.Ok) + throw CreatePop3Exception (this); + + if (Exception != null) + throw Exception; + } } } diff --git a/MailKit/Net/Pop3/Pop3CommandException.cs b/MailKit/Net/Pop3/Pop3CommandException.cs index 1d778c0840..2d683a2443 100644 --- a/MailKit/Net/Pop3/Pop3CommandException.cs +++ b/MailKit/Net/Pop3/Pop3CommandException.cs @@ -1,9 +1,9 @@ -// +// // Pop3CommandException.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -56,9 +56,10 @@ public class Pop3CommandException : CommandException /// The serialization info. /// The streaming context. /// - /// is null. + /// is . /// [SecuritySafeCritical] + [Obsolete ("This API supports obsolete formatter-based serialization. It should not be called or extended by application code.")] protected Pop3CommandException (SerializationInfo info, StreamingContext context) : base (info, context) { StatusText = info.GetString ("StatusText"); @@ -88,7 +89,7 @@ public Pop3CommandException (string message, Exception innerException) : base (m /// The response status text. /// An inner exception. /// - /// is null. + /// is . /// public Pop3CommandException (string message, string statusText, Exception innerException) : base (message, innerException) { @@ -119,7 +120,7 @@ public Pop3CommandException (string message) : base (message) /// The error message. /// The response status text. /// - /// is null. + /// is . /// public Pop3CommandException (string message, string statusText) : base (message) { @@ -165,9 +166,12 @@ public string StatusText { /// The serialization info. /// The streaming context. /// - /// is null. + /// is . /// [SecurityCritical] +#if NET8_0_OR_GREATER + [Obsolete ("This API supports obsolete formatter-based serialization. It should not be called or extended by application code.")] +#endif public override void GetObjectData (SerializationInfo info, StreamingContext context) { base.GetObjectData (info, context); diff --git a/MailKit/Net/Pop3/Pop3Engine.cs b/MailKit/Net/Pop3/Pop3Engine.cs index a1fc47ef6f..57227fc98c 100644 --- a/MailKit/Net/Pop3/Pop3Engine.cs +++ b/MailKit/Net/Pop3/Pop3Engine.cs @@ -1,9 +1,9 @@ -// +// // Pop3Engine.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -25,17 +25,14 @@ // using System; -using System.IO; using System.Text; using System.Threading; +using System.Diagnostics; +using System.Net.Security; +using System.Globalization; +using System.Threading.Tasks; using System.Collections.Generic; - -#if NETFX_CORE -using Encoding = Portable.Text.Encoding; -using EncoderExceptionFallback = Portable.Text.EncoderExceptionFallback; -using DecoderExceptionFallback = Portable.Text.DecoderExceptionFallback; -using DecoderFallbackException = Portable.Text.DecoderFallbackException; -#endif +using System.Diagnostics.CodeAnalysis; namespace MailKit.Net.Pop3 { /// @@ -64,33 +61,26 @@ enum Pop3EngineState { /// class Pop3Engine { - static readonly Encoding Latin1; - static readonly Encoding UTF8; - +#if NET6_0_OR_GREATER + readonly ClientMetrics? metrics; +#endif readonly List queue; - Pop3Stream stream; - int nextId; - - static Pop3Engine () - { - UTF8 = Encoding.GetEncoding (65001, new EncoderExceptionFallback (), new DecoderExceptionFallback ()); - - try { - Latin1 = Encoding.GetEncoding (28591); - } catch (NotSupportedException) { - Latin1 = Encoding.GetEncoding (1252); - } - } + long clientConnectedTimestamp; + bool secure; /// /// Initializes a new instance of the class. /// public Pop3Engine () { - AuthenticationMechanisms = new HashSet (); + AuthenticationMechanisms = new HashSet (StringComparer.Ordinal); Capabilities = Pop3Capabilities.User; queue = new List (); - nextId = 1; + +#if NET6_0_OR_GREATER + // Use the globally configured Pop3Client metrics. + metrics = Telemetry.Pop3Client.Metrics; +#endif } /// @@ -100,7 +90,7 @@ public Pop3Engine () /// Gets the URI of the POP3 server. /// /// The URI of the POP3 server. - public Uri Uri { + public Uri? Uri { get; internal set; } @@ -108,8 +98,8 @@ public Uri Uri { /// Gets the authentication mechanisms supported by the POP3 server. /// /// - /// The authentication mechanisms are queried durring the - /// method. + /// The authentication mechanisms are queried during the + /// method. /// /// The authentication mechanisms. public HashSet AuthenticationMechanisms { @@ -121,7 +111,7 @@ public HashSet AuthenticationMechanisms { /// /// /// The capabilities will not be known until a successful connection - /// has been made via the method. + /// has been made via the method. /// /// The capabilities. public Pop3Capabilities Capabilities { @@ -135,8 +125,8 @@ public Pop3Capabilities Capabilities { /// Gets the underlying POP3 stream. /// /// The pop3 stream. - public Pop3Stream Stream { - get { return stream; } + public Pop3Stream? Stream { + get; private set; } /// @@ -156,9 +146,23 @@ public Pop3EngineState State { /// /// Gets whether or not the engine is currently connected to a POP3 server. /// - /// true if the engine is connected; otherwise, false. + /// if the engine is connected; otherwise, . + [MemberNotNullWhen (true, new[] { nameof (Stream), nameof (Uri) })] public bool IsConnected { - get { return stream != null && stream.IsConnected; } + get { return Stream != null && Stream.IsConnected; } + } + + /// + /// Get whether or not the connection is secure (typically via SSL or TLS). + /// + /// + /// Gets whether or not the connection is secure (typically via SSL or TLS). + /// + /// if the connection is secure; otherwise, . + [MemberNotNullWhen (true, new[] { nameof (Stream), nameof (Uri) })] + public bool IsSecure { + get { return IsConnected && secure; } + set { secure = value; } } /// @@ -168,7 +172,7 @@ public bool IsConnected { /// Gets the APOP authentication token. /// /// The APOP authentication token. - public string ApopToken { + public string? ApopToken { get; private set; } @@ -190,7 +194,7 @@ public int ExpirePolicy { /// Gets the implementation details of the server. /// /// The implementation details. - public string Implementation { + public string? Implementation { get; private set; } @@ -205,28 +209,30 @@ public int LoginDelay { get; private set; } - /// - /// Takes posession of the and reads the greeting. - /// - /// - /// Takes posession of the and reads the greeting. - /// - /// The pop3 stream. - /// The cancellation token - public void Connect (Pop3Stream pop3, CancellationToken cancellationToken) + [MemberNotNull (nameof (Stream))] + internal void CheckConnected () { - if (stream != null) - stream.Dispose (); + if (Stream == null) + throw new InvalidOperationException (); + } + [MemberNotNull (nameof (Stream))] + void Initialize (Pop3Stream pop3) + { + Stream?.Dispose (); + + clientConnectedTimestamp = Stopwatch.GetTimestamp (); Capabilities = Pop3Capabilities.User; AuthenticationMechanisms.Clear (); State = Pop3EngineState.Disconnected; ApopToken = null; - stream = pop3; - // read the pop3 server greeting - var greeting = ReadLine (cancellationToken).TrimEnd (); + secure = pop3.Stream is SslStream; + Stream = pop3; + } + void ParseGreeting (string greeting) + { int index = greeting.IndexOf (' '); string token, text; @@ -246,29 +252,83 @@ public void Connect (Pop3Stream pop3, CancellationToken cancellationToken) } if (token != "+OK") { - stream.Dispose (); - stream = null; + Stream!.Dispose (); + Stream = null; throw new Pop3ProtocolException (string.Format ("Unexpected greeting from server: {0}", greeting)); } - index = text.IndexOf ('>'); - if (text.Length > 0 && text[0] == '<' && index != -1) { - ApopToken = text.Substring (0, index + 1); - Capabilities |= Pop3Capabilities.Apop; + index = text.IndexOf ('<'); + if (index != -1 && index + 1 < text.Length) { + int endIndex = text.IndexOf ('>', index + 1); + + if (endIndex++ != -1) { + ApopToken = text.Substring (index, endIndex - index); + Capabilities |= Pop3Capabilities.Apop; + } } State = Pop3EngineState.Connected; } - public event EventHandler Disconnected; + public NetworkOperation StartNetworkOperation (NetworkOperationKind kind, Uri? uri = null) + { +#if NET6_0_OR_GREATER + return NetworkOperation.Start (kind, uri ?? Uri!, Telemetry.Pop3Client.ActivitySource, metrics); +#else + return NetworkOperation.Start (kind, uri ?? Uri!); +#endif + } + + /// + /// Takes possession of the and reads the greeting. + /// + /// + /// Takes possession of the and reads the greeting. + /// + /// The pop3 stream. + /// The cancellation token + public void Connect (Pop3Stream pop3, CancellationToken cancellationToken) + { + Initialize (pop3); + + // read the pop3 server greeting + var greeting = ReadLine (cancellationToken).TrimEnd (); + + ParseGreeting (greeting); + } + + /// + /// Takes possession of the and reads the greeting. + /// + /// + /// Takes possession of the and reads the greeting. + /// + /// The pop3 stream. + /// The cancellation token + public async Task ConnectAsync (Pop3Stream pop3, CancellationToken cancellationToken) + { + Initialize (pop3); + + // read the pop3 server greeting + var greeting = (await ReadLineAsync (cancellationToken).ConfigureAwait (false)).TrimEnd (); + + ParseGreeting (greeting); + } + + public event EventHandler? Disconnected; void OnDisconnected () { - var handler = Disconnected; + Disconnected?.Invoke (this, EventArgs.Empty); + } - if (handler != null) - handler (this, EventArgs.Empty); + void RecordClientDisconnected (Exception? ex) + { +#if NET6_0_OR_GREATER + metrics?.RecordClientDisconnected (clientConnectedTimestamp, Uri!, ex); +#endif + clientConnectedTimestamp = 0; } /// @@ -277,15 +337,18 @@ void OnDisconnected () /// /// Disconnects the . /// - public void Disconnect () + /// The exception that is causing the disconnection. + public void Disconnect (Exception? ex) { - Uri = null; + RecordClientDisconnected (ex); - if (stream != null) { - stream.Dispose (); - stream = null; + if (Stream != null) { + Stream.Dispose (); + Stream = null; } + secure = false; + if (State != Pop3EngineState.Disconnected) { State = Pop3EngineState.Disconnected; OnDisconnected (); @@ -308,38 +371,51 @@ public void Disconnect () /// public string ReadLine (CancellationToken cancellationToken) { - if (stream == null) - throw new InvalidOperationException (); + CheckConnected (); - using (var memory = new MemoryStream ()) { - int offset, count; - byte[] buf; + using (var builder = new ByteArrayBuilder (64)) { + bool complete; - while (!stream.ReadLine (out buf, out offset, out count, cancellationToken)) - memory.Write (buf, offset, count); + do { + complete = Stream.ReadLine (builder, cancellationToken); + } while (!complete); - memory.Write (buf, offset, count); + // FIXME: All callers expect CRLF to be trimmed, but many also want all trailing whitespace trimmed. + builder.TrimNewLine (); - count = (int) memory.Length; -#if !NETFX_CORE && !NETSTANDARD - buf = memory.GetBuffer (); -#else - buf = memory.ToArray (); -#endif + return builder.ToString (); + } + } - // Trim the sequence from the end of the line. - if (buf[count - 1] == (byte) '\n') { - count--; + /// + /// Reads a single line from the . + /// + /// The line. + /// The cancellation token. + /// + /// The engine is not connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public async Task ReadLineAsync (CancellationToken cancellationToken) + { + CheckConnected (); - if (buf[count - 1] == (byte) '\r') - count--; - } + using (var builder = new ByteArrayBuilder (64)) { + bool complete; - try { - return UTF8.GetString (buf, 0, count); - } catch (DecoderFallbackException) { - return Latin1.GetString (buf, 0, count); - } + do { + complete = await Stream.ReadLineAsync (builder, cancellationToken).ConfigureAwait (false); + } while (!complete); + + // FIXME: All callers expect CRLF to be trimmed, but many also want all trailing whitespace trimmed. + builder.TrimNewLine (); + + return builder.ToString (); } } @@ -375,40 +451,69 @@ public static Pop3CommandStatus GetCommandStatus (string response, out string te return Pop3CommandStatus.ProtocolError; } - void SendCommand (Pop3Command pc) + void ReadResponse (Pop3Command pc, CancellationToken cancellationToken) { - var buf = pc.Encoding.GetBytes (pc.Command + "\r\n"); + string response; + + try { + response = ReadLine (cancellationToken).TrimEnd (); + } catch (Exception ex) { + pc.Status = Pop3CommandStatus.ProtocolError; + Disconnect (ex); + throw; + } + + pc.Status = GetCommandStatus (response, out string text); + pc.StatusText = text; - stream.Write (buf, 0, buf.Length); + switch (pc.Status) { + case Pop3CommandStatus.ProtocolError: + var pex = new Pop3ProtocolException (string.Format ("Unexpected response from server: {0}", response)); + Disconnect (pex); + throw pex; + case Pop3CommandStatus.Continue: + case Pop3CommandStatus.Ok: + if (pc.Handler != null) { + try { + pc.Handler (this, pc, text, false, cancellationToken); + } catch (Exception ex) { + pc.Status = Pop3CommandStatus.ProtocolError; + Disconnect (ex); + throw; + } + } + break; + } } - void ReadResponse (Pop3Command pc) + async Task ReadResponseAsync (Pop3Command pc, CancellationToken cancellationToken) { - string response, text; + string response; try { - response = ReadLine (pc.CancellationToken).TrimEnd (); - } catch { + response = (await ReadLineAsync (cancellationToken).ConfigureAwait (false)).TrimEnd (); + } catch (Exception ex) { pc.Status = Pop3CommandStatus.ProtocolError; - Disconnect (); + Disconnect (ex); throw; } - pc.Status = GetCommandStatus (response, out text); + pc.Status = GetCommandStatus (response, out string text); pc.StatusText = text; switch (pc.Status) { case Pop3CommandStatus.ProtocolError: - Disconnect (); - throw new Pop3ProtocolException (string.Format ("Unexpected response from server: {0}", response)); + var pex = new Pop3ProtocolException (string.Format ("Unexpected response from server: {0}", response)); + Disconnect (pex); + throw pex; case Pop3CommandStatus.Continue: case Pop3CommandStatus.Ok: if (pc.Handler != null) { try { - pc.Handler (this, pc, text); - } catch { + await pc.Handler (this, pc, text, true, cancellationToken).ConfigureAwait (false); + } catch (Exception ex) { pc.Status = Pop3CommandStatus.ProtocolError; - Disconnect (); + Disconnect (ex); throw; } } @@ -416,164 +521,276 @@ void ReadResponse (Pop3Command pc) } } - public int Iterate () + [MemberNotNull (nameof (Stream))] + void CheckCanRun (CancellationToken cancellationToken) { - if (stream == null) - throw new InvalidOperationException (); - - if (queue.Count == 0) - return 0; - - int count = (Capabilities & Pop3Capabilities.Pipelining) != 0 ? queue.Count : 1; - var cancellationToken = queue[0].CancellationToken; - var active = new List (); + CheckConnected (); if (cancellationToken.IsCancellationRequested) { - queue.RemoveAll (x => x.CancellationToken.IsCancellationRequested); + queue.Clear (); cancellationToken.ThrowIfCancellationRequested (); } + } - for (int i = 0; i < count; i++) { - var pc = queue[0]; + /// + /// Run the command pipeline. + /// + /// if exceptions should be thrown for failed commands; otherwise, . + /// The cancellation token. + /// + /// The engine is not connected. + /// + public void Run (bool throwOnError, CancellationToken cancellationToken) + { + CheckCanRun (cancellationToken); - if (i > 0 && !pc.CancellationToken.Equals (cancellationToken)) - break; + try { + for (int i = 0; i < queue.Count; i++) { + var pc = queue[i]; - queue.RemoveAt (0); + pc.Status = Pop3CommandStatus.Active; - pc.Status = Pop3CommandStatus.Active; - active.Add (pc); + Stream.QueueCommand (pc.Encoding, pc.Command, cancellationToken); + } + + Stream.Flush (cancellationToken); - SendCommand (pc); + for (int i = 0; i < queue.Count; i++) + ReadResponse (queue[i], cancellationToken); + + for (int i = 0; i < queue.Count && throwOnError; i++) + queue[i].ThrowIfError (); + } finally { + queue.Clear (); } + } + + /// + /// Asynchronously run the command pipeline. + /// + /// if exceptions should be thrown for failed commands; otherwise, . + /// The cancellation token. + /// + /// The engine is not connected. + /// + public async Task RunAsync (bool throwOnError, CancellationToken cancellationToken) + { + CheckCanRun (cancellationToken); - stream.Flush (cancellationToken); + try { + for (int i = 0; i < queue.Count; i++) { + var pc = queue[i]; + + pc.Status = Pop3CommandStatus.Active; - for (int i = 0; i < active.Count; i++) - ReadResponse (active[i]); + await Stream.QueueCommandAsync (pc.Encoding, pc.Command, cancellationToken).ConfigureAwait (false); + } - return active[active.Count - 1].Id; + await Stream.FlushAsync (cancellationToken).ConfigureAwait (false); + + for (int i = 0; i < queue.Count; i++) + await ReadResponseAsync (queue[i], cancellationToken).ConfigureAwait (false); + + for (int i = 0; i < queue.Count && throwOnError; i++) + queue[i].ThrowIfError (); + } finally { + queue.Clear (); + } } - public Pop3Command QueueCommand (CancellationToken cancellationToken, Pop3CommandHandler handler, Encoding encoding, string format, params object[] args) + public Pop3Command QueueCommand (Pop3CommandHandler? handler, Encoding encoding, string format, params object[] args) { - var pc = new Pop3Command (cancellationToken, handler, encoding, format, args); - pc.Id = nextId++; + var pc = new Pop3Command (handler, encoding, format, args); queue.Add (pc); return pc; } - public Pop3Command QueueCommand (CancellationToken cancellationToken, Pop3CommandHandler handler, string format, params object[] args) + public Pop3Command QueueCommand (Pop3CommandHandler? handler, string format, params object[] args) { - return QueueCommand (cancellationToken, handler, Encoding.ASCII, format, args); + return QueueCommand (handler, Encoding.ASCII, format, args); } - static void CapaHandler (Pop3Engine engine, Pop3Command pc, string text) + static bool IsCapability (string capability, string text, int length, bool hasValue = false) { - if (pc.Status != Pop3CommandStatus.Ok) - return; + if (hasValue) { + if (length < capability.Length) + return false; + } else { + if (length != capability.Length) + return false; + } - string response; + if (string.Compare (text, 0, capability, 0, capability.Length, StringComparison.OrdinalIgnoreCase) != 0) + return false; - do { - if ((response = engine.ReadLine (pc.CancellationToken)) == ".") - break; + if (hasValue) { + int index = capability.Length; - int index = response.IndexOf (' '); - string token, data; - int value; + return length == capability.Length || text[index] == ' ' || text[index] == '='; + } - if (index != -1) { - token = response.Substring (0, index); + return true; + } - while (index < response.Length && char.IsWhiteSpace (response[index])) - index++; + static bool IsToken (string token, string text, int startIndex, int length) + { + return length == token.Length && string.Compare (text, startIndex, token, 0, token.Length, StringComparison.OrdinalIgnoreCase) == 0; + } - if (index < response.Length) - data = response.Substring (index); - else - data = string.Empty; - } else { - data = string.Empty; - token = response; - } + static bool ReadNextToken (string text, ref int index, out int startIndex, out int length) + { + while (index < text.Length && char.IsWhiteSpace (text[index])) + index++; - switch (token) { - case "EXPIRE": - engine.Capabilities |= Pop3Capabilities.Expire; - var tokens = data.Split (' '); + startIndex = index; - if (int.TryParse (tokens[0], out value)) - engine.ExpirePolicy = value; - else if (tokens[0] == "NEVER") + while (index < text.Length && !char.IsWhiteSpace (text[index])) + index++; + + length = index - startIndex; + + return length > 0; + } + + void AddAuthenticationMechanisms (string text, int startIndex) + { + int index = startIndex; + + while (ReadNextToken (text, ref index, out var tokenIndex, out var length)) { + var mechanism = text.Substring (tokenIndex, length); + + AuthenticationMechanisms.Add (mechanism); + } + } + + static bool TryParseInt32 (string text, int startIndex, int length, out int value) + { +#if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER + var token = text.AsSpan (startIndex, length); +#else + var token = text.Substring (startIndex, length); +#endif + + return int.TryParse (token, NumberStyles.None, CultureInfo.InvariantCulture, out value); + } + + static void ParseCapaResponse (Pop3Engine engine, string response) + { + int index = response.IndexOf (' '); + int startIndex, length, value; + + if (index == -1) + index = response.Length; + + if (IsCapability ("EXPIRE", response, index, true)) { + engine.Capabilities |= Pop3Capabilities.Expire; + + if (ReadNextToken (response, ref index, out startIndex, out length)) { + if (IsToken ("NEVER", response, startIndex, length)) { engine.ExpirePolicy = -1; - break; - case "IMPLEMENTATION": - engine.Implementation = data; - break; - case "LOGIN-DELAY": - if (int.TryParse (data, out value)) { + } else if (TryParseInt32 (response, startIndex, length, out value)) { + engine.ExpirePolicy = value; + } + } + } else if (IsCapability ("IMPLEMENTATION", response, index, true)) { + engine.Implementation = response.Substring (index + 1); + } else if (IsCapability ("LANG", response, index)) { + engine.Capabilities |= Pop3Capabilities.Lang; + } else if (IsCapability ("LOGIN-DELAY", response, index, true)) { + if (ReadNextToken (response, ref index, out startIndex, out length)) { + if (TryParseInt32 (response, startIndex, length, out value)) { engine.Capabilities |= Pop3Capabilities.LoginDelay; engine.LoginDelay = value; } - break; - case "PIPELINING": - engine.Capabilities |= Pop3Capabilities.Pipelining; - break; - case "RESP-CODES": - engine.Capabilities |= Pop3Capabilities.ResponseCodes; - break; - case "SASL": - engine.Capabilities |= Pop3Capabilities.Sasl; - foreach (var authmech in data.Split (new [] { ' ' }, StringSplitOptions.RemoveEmptyEntries)) - engine.AuthenticationMechanisms.Add (authmech); - break; - case "STLS": - engine.Capabilities |= Pop3Capabilities.StartTLS; - break; - case "TOP": - engine.Capabilities |= Pop3Capabilities.Top; - break; - case "UIDL": - engine.Capabilities |= Pop3Capabilities.UIDL; - break; - case "USER": - engine.Capabilities |= Pop3Capabilities.User; - break; - case "UTF8": - engine.Capabilities |= Pop3Capabilities.UTF8; - - foreach (var item in data.Split (' ')) { - if (item == "USER") - engine.Capabilities |= Pop3Capabilities.UTF8User; + } + } else if (IsCapability ("PIPELINING", response, index)) { + engine.Capabilities |= Pop3Capabilities.Pipelining; + } else if (IsCapability ("RESP-CODES", response, index)) { + engine.Capabilities |= Pop3Capabilities.ResponseCodes; + } else if (IsCapability ("SASL", response, index, true)) { + engine.Capabilities |= Pop3Capabilities.Sasl; + engine.AddAuthenticationMechanisms (response, index); + } else if (IsCapability ("STLS", response, index)) { + engine.Capabilities |= Pop3Capabilities.StartTLS; + } else if (IsCapability ("TOP", response, index)) { + engine.Capabilities |= Pop3Capabilities.Top; + } else if (IsCapability ("UIDL", response, index)) { + engine.Capabilities |= Pop3Capabilities.UIDL; + } else if (IsCapability ("USER", response, index)) { + engine.Capabilities |= Pop3Capabilities.User; + } else if (IsCapability ("UTF8", response, index, true)) { + engine.Capabilities |= Pop3Capabilities.UTF8; + + while (ReadNextToken (response, ref index, out startIndex, out length)) { + if (IsToken ("USER", response, startIndex, length)) { + engine.Capabilities |= Pop3Capabilities.UTF8User; } + } + } + } + + static void ReadCapaResponse (Pop3Engine engine, Pop3Command pc, CancellationToken cancellationToken) + { + string response; + + do { + if ((response = engine.ReadLine (cancellationToken)) == ".") break; - case "LANG": - engine.Capabilities |= Pop3Capabilities.Lang; + + ParseCapaResponse (engine, response); + } while (true); + } + + static async Task ReadCapaResponseAsync (Pop3Engine engine, Pop3Command pc, CancellationToken cancellationToken) + { + string response; + + do { + if ((response = await engine.ReadLineAsync (cancellationToken).ConfigureAwait (false)) == ".") break; - } + + ParseCapaResponse (engine, response); } while (true); } - public Pop3CommandStatus QueryCapabilities (CancellationToken cancellationToken) + static Task ProcessCapaResponse (Pop3Engine engine, Pop3Command pc, string text, bool doAsync, CancellationToken cancellationToken) { - if (stream == null) - throw new InvalidOperationException (); + if (pc.Status != Pop3CommandStatus.Ok) + return Task.CompletedTask; + + if (doAsync) + return ReadCapaResponseAsync (engine, pc, cancellationToken); + + ReadCapaResponse (engine, pc, cancellationToken); + return Task.CompletedTask; + } + + Pop3Command QueueCapabilitiesCommand () + { + CheckConnected (); - // clear all CAPA response capabilities (except the APOP capability) - Capabilities &= Pop3Capabilities.Apop; + // Clear all CAPA response capabilities (except the APOP, USER, and STLS capabilities). + Capabilities &= Pop3Capabilities.Apop | Pop3Capabilities.User | Pop3Capabilities.StartTLS; AuthenticationMechanisms.Clear (); Implementation = null; ExpirePolicy = 0; LoginDelay = 0; - var pc = QueueCommand (cancellationToken, CapaHandler, "CAPA"); + return QueueCommand (ProcessCapaResponse, "CAPA\r\n"); + } - while (Iterate () < pc.Id) { - // continue processing commands... - } + public void QueryCapabilities (CancellationToken cancellationToken) + { + QueueCapabilitiesCommand (); + + Run (false, cancellationToken); + } + + public Task QueryCapabilitiesAsync (CancellationToken cancellationToken) + { + QueueCapabilitiesCommand (); - return pc.Status; + return RunAsync (false, cancellationToken); } } } diff --git a/MailKit/Net/Pop3/Pop3Language.cs b/MailKit/Net/Pop3/Pop3Language.cs index b42edb92b0..d04cf34389 100644 --- a/MailKit/Net/Pop3/Pop3Language.cs +++ b/MailKit/Net/Pop3/Pop3Language.cs @@ -1,9 +1,9 @@ -// +// // Pop3Language.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal diff --git a/MailKit/Net/Pop3/Pop3ProtocolException.cs b/MailKit/Net/Pop3/Pop3ProtocolException.cs index f9a5bfdfc4..f9b82ad93c 100644 --- a/MailKit/Net/Pop3/Pop3ProtocolException.cs +++ b/MailKit/Net/Pop3/Pop3ProtocolException.cs @@ -1,9 +1,9 @@ -// +// // Pop3ProtocolException.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -57,9 +57,10 @@ public class Pop3ProtocolException : ProtocolException /// The serialization info. /// The streaming context. /// - /// is null. + /// is . /// [SecuritySafeCritical] + [Obsolete ("This API supports obsolete formatter-based serialization. It should not be called or extended by application code.")] protected Pop3ProtocolException (SerializationInfo info, StreamingContext context) : base (info, context) { } diff --git a/MailKit/Net/Pop3/Pop3Stream.cs b/MailKit/Net/Pop3/Pop3Stream.cs index ea7e9fbb74..2f0d8514f7 100644 --- a/MailKit/Net/Pop3/Pop3Stream.cs +++ b/MailKit/Net/Pop3/Pop3Stream.cs @@ -1,9 +1,9 @@ -// +// // Pop3Stream.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -26,20 +26,15 @@ using System; using System.IO; +using System.Text; using System.Threading; -using Buffer = System.Buffer; - -#if NETFX_CORE -using Windows.Storage.Streams; -using Windows.Networking.Sockets; -using Socket = Windows.Networking.Sockets.StreamSocket; -#else -using System.Net.Security; using System.Net.Sockets; -#endif +using System.Threading.Tasks; using MimeKit.IO; +using Buffer = System.Buffer; + namespace MailKit.Net.Pop3 { /// /// An enumeration of the possible POP3 streaming modes. @@ -95,36 +90,32 @@ class Pop3Stream : Stream, ICancellableStream /// Creates a new . /// /// The underlying network stream. - /// The underlying network socket. /// The protocol logger. - public Pop3Stream (Stream source, Socket socket, IProtocolLogger protocolLogger) + public Pop3Stream (Stream source, IProtocolLogger protocolLogger) { logger = protocolLogger; IsConnected = true; Stream = source; - Socket = socket; } /// - /// Get or sets the underlying network stream. + /// Get the underlying network stream. /// /// - /// Gets or sets the underlying network stream. + /// Gets the underlying network stream. /// /// The underlying network stream. public Stream Stream { - get; internal set; + get; private set; } - /// - /// Get the underlying network socket. - /// - /// - /// Gets the underlying network socket. - /// - /// The underlying network socket. - public Socket Socket { - get; private set; + internal void SetStream (Stream stream) + { + Stream = stream; + + // reset internal buffering + inputIndex = ReadAheadSize; + inputEnd = ReadAheadSize; } /// @@ -145,7 +136,7 @@ public Pop3StreamMode Mode { /// /// Gets whether or not the stream is connected. /// - /// true if the stream is connected; otherwise, false. + /// if the stream is connected; otherwise, . public bool IsConnected { get; private set; } @@ -154,9 +145,9 @@ public bool IsConnected { /// Get whether or not the end of the raw data has been reached in mode. /// /// - /// When reading the resonse to a command such as RETR, the end of the data is marked by line matching ".\r\n". + /// When reading the response to a command such as RETR, the end of the data is marked by line matching ".\r\n". /// - /// true if the end of the data has been reached; otherwise, false. + /// if the end of the data has been reached; otherwise, . public bool IsEndOfData { get; private set; } @@ -167,7 +158,7 @@ public bool IsEndOfData { /// /// Gets whether the stream supports reading. /// - /// true if the stream supports reading; otherwise, false. + /// if the stream supports reading; otherwise, . public override bool CanRead { get { return Stream.CanRead; } } @@ -178,7 +169,7 @@ public override bool CanRead { /// /// Gets whether the stream supports writing. /// - /// true if the stream supports writing; otherwise, false. + /// if the stream supports writing; otherwise, . public override bool CanWrite { get { return Stream.CanWrite; } } @@ -189,7 +180,7 @@ public override bool CanWrite { /// /// Gets whether the stream supports seeking. /// - /// true if the stream supports seeking; otherwise, false. + /// if the stream supports seeking; otherwise, . public override bool CanSeek { get { return false; } } @@ -200,7 +191,7 @@ public override bool CanSeek { /// /// Gets whether the stream supports I/O timeouts. /// - /// true if the stream supports I/O timeouts; otherwise, false. + /// if the stream supports I/O timeouts; otherwise, . public override bool CanTimeout { get { return Stream.CanTimeout; } } @@ -250,7 +241,7 @@ public override int WriteTimeout { /// public override long Position { get { return Stream.Position; } - set { Stream.Position = value; } + set { throw new NotSupportedException (); } } /// @@ -271,31 +262,12 @@ public override long Length { get { return Stream.Length; } } - void Poll (SelectMode mode, CancellationToken cancellationToken) - { -#if NETFX_CORE - cancellationToken.ThrowIfCancellationRequested (); -#else - if (!cancellationToken.CanBeCanceled) - return; - - if (Socket != null) { - do { - cancellationToken.ThrowIfCancellationRequested (); - // wait 1/4 second and then re-check for cancellation - } while (!Socket.Poll (250000, mode)); - } else { - cancellationToken.ThrowIfCancellationRequested (); - } -#endif - } - - unsafe int ReadAhead (CancellationToken cancellationToken) + void AlignReadAheadBuffer (out int start, out int end) { int left = inputEnd - inputIndex; - int start = inputStart; - int end = inputEnd; - int nread; + + start = inputStart; + end = inputEnd; if (left > 0) { int index = inputIndex; @@ -324,30 +296,52 @@ unsafe int ReadAhead (CancellationToken cancellationToken) } end = input.Length - PadSize; + } + + void OnReadAhead (int start, int nread) + { + if (nread > 0) { + logger.LogServer (input, start, nread); + inputEnd += nread; + } else { + throw new Pop3ProtocolException ("The POP3 server has unexpectedly disconnected."); + } + } + + int ReadAhead (CancellationToken cancellationToken) + { + AlignReadAheadBuffer (out int start, out int end); try { -#if !NETFX_CORE - bool buffered = !(Stream is NetworkStream); -#else - bool buffered = true; -#endif + var network = Stream as NetworkStream; + int nread; - if (buffered) { - cancellationToken.ThrowIfCancellationRequested (); + cancellationToken.ThrowIfCancellationRequested (); - nread = Stream.Read (input, start, end - start); - } else { - Poll (SelectMode.SelectRead, cancellationToken); + network?.Poll (SelectMode.SelectRead, cancellationToken); + nread = Stream.Read (input, start, end - start); - nread = Stream.Read (input, start, end - start); - } + OnReadAhead (start, nread); + } catch { + IsConnected = false; + throw; + } - if (nread > 0) { - logger.LogServer (input, start, nread); - inputEnd += nread; - } else { - throw new Pop3ProtocolException ("The POP3 server has unexpectedly disconnected."); - } + return inputEnd - inputIndex; + } + + async Task ReadAheadAsync (CancellationToken cancellationToken) + { + AlignReadAheadBuffer (out int start, out int end); + + try { + int nread; + + cancellationToken.ThrowIfCancellationRequested (); + + nread = await Stream.ReadAsync (input, start, end - start, cancellationToken).ConfigureAwait (false); + + OnReadAhead (start, nread); } catch { IsConnected = false; throw; @@ -374,14 +368,70 @@ void CheckDisposed () throw new ObjectDisposedException (nameof (Pop3Stream)); } - unsafe bool NeedInput (byte* inptr, int inputLeft) + bool NeedInput (int index, int inputLeft) { - if (inputLeft == 2 && *inptr == (byte) '.' && *(inptr + 1) == '\n') + if (inputLeft == 2 && input[index] == (byte) '.' && input[index + 1] == '\n') return false; return true; } + void Read (byte[] buffer, ref int index, int endIndex) + { + // terminate the input buffer with a '\n' to remove bounds checking in our inner loop + input[inputEnd] = (byte) '\n'; + + while (inputIndex < inputEnd) { + if (midline) { + // read until end-of-line + while (index < endIndex && input[inputIndex] != (byte) '\n') + buffer[index++] = input[inputIndex++]; + + if (inputIndex == inputEnd || index == endIndex) + break; + + // consume the '\n' character + buffer[index++] = input[inputIndex++]; + midline = false; + } + + if (inputIndex == inputEnd) + break; + + if (input[inputIndex] == (byte) '.') { + int inputLeft = inputEnd - inputIndex; + + // check for ".\r\n" which signifies the end of the data stream + if (inputLeft >= 3 && input[inputIndex + 1] == (byte) '\r' && input[inputIndex + 2] == (byte) '\n') { + IsEndOfData = true; + midline = false; + inputIndex += 3; + break; + } + + // check for ".\n" which is used by some broken UNIX servers in place of ".\r\n" + if (inputLeft >= 2 && input[inputIndex + 1] == (byte) '\n') { + IsEndOfData = true; + midline = false; + inputIndex += 2; + break; + } + + // check for "." or ".\r" which might be an incomplete termination sequence + if (inputLeft == 1 || (inputLeft == 2 && input[inputIndex + 1] == (byte) '\r')) { + // not enough data... + break; + } + + // check for lines beginning with ".." which should be transformed into "." + if (input[inputIndex + 1] == (byte) '.') + inputIndex++; + } + + midline = true; + } + } + /// /// Reads a sequence of bytes from the stream and advances the position /// within the stream by the number of bytes read. @@ -397,12 +447,12 @@ unsafe bool NeedInput (byte* inptr, int inputLeft) /// The number of bytes to read. /// The cancellation token. /// - /// is null. + /// is . /// /// /// is less than zero or greater than the length of . /// -or- - /// The is not large enough to contain bytes strting + /// The is not large enough to contain bytes starting /// at the specified . /// /// @@ -429,81 +479,25 @@ public int Read (byte[] buffer, int offset, int count, CancellationToken cancell if (IsEndOfData || count == 0) return 0; - unsafe { - fixed (byte* inbuf = input, bufptr = buffer) { - byte* outbuf = bufptr + offset; - byte* outend = outbuf + count; - byte* outptr = outbuf; - byte* inptr, inend; - int inputLeft; - - do { - inputLeft = inputEnd - inputIndex; - inptr = inbuf + inputIndex; - - // we need at least 3 bytes: ".\r\n" - if (inputLeft < 3 && (midline || NeedInput (inptr, inputLeft))) { - if (outptr > outbuf) - break; - - ReadAhead (cancellationToken); - inptr = inbuf + inputIndex; - } - - inend = inbuf + inputEnd; - *inend = (byte) '\n'; + int endIndex = offset + count; + int index = offset; + int inputLeft; - while (inptr < inend) { - if (midline) { - // read until end-of-line - while (outptr < outend && *inptr != (byte) '\n') - *outptr++ = *inptr++; + do { + inputLeft = inputEnd - inputIndex; - if (inptr == inend || outptr == outend) - break; + // we need at least 3 bytes: ".\r\n" + if (inputLeft < 3 && (midline || NeedInput (inputIndex, inputLeft))) { + if (index > offset) + break; - *outptr++ = *inptr++; - midline = false; - } - - if (inptr == inend) - break; - - if (*inptr == (byte) '.') { - inputLeft = (int) (inend - inptr); - - if (inputLeft >= 3 && *(inptr + 1) == (byte) '\r' && *(inptr + 2) == (byte) '\n') { - IsEndOfData = true; - midline = false; - inptr += 3; - break; - } - - if (inputLeft >= 2 && *(inptr + 1) == (byte) '\n') { - IsEndOfData = true; - midline = false; - inptr += 2; - break; - } - - if (inputLeft == 1 || (inputLeft == 2 && *(inptr + 1) == (byte) '\r')) { - // not enough data... - break; - } - - if (*(inptr + 1) == (byte) '.') - inptr++; - } - - midline = true; - } + ReadAhead (cancellationToken); + } - inputIndex = (int) (inptr - inbuf); - } while (outptr < outend && !IsEndOfData); + Read (buffer, ref index, endIndex); + } while (index < endIndex && !IsEndOfData); - return (int) (outptr - outbuf); - } - } + return index - offset; } /// @@ -520,12 +514,12 @@ public int Read (byte[] buffer, int offset, int count, CancellationToken cancell /// The buffer offset. /// The number of bytes to read. /// - /// is null. + /// is . /// /// /// is less than zero or greater than the length of . /// -or- - /// The is not large enough to contain bytes strting + /// The is not large enough to contain bytes starting /// at the specified . /// /// @@ -543,38 +537,80 @@ public override int Read (byte[] buffer, int offset, int count) } /// - /// Reads a single line of input from the stream. + /// Reads a sequence of bytes from the stream and advances the position + /// within the stream by the number of bytes read. /// /// - /// This method should be called in a loop until it returns true. + /// Reads a sequence of bytes from the stream and advances the position + /// within the stream by the number of bytes read. /// - /// true, if reading the line is complete, false otherwise. - /// The buffer containing the line data. - /// The offset into the buffer containing bytes read. - /// The number of bytes read. + /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many + /// bytes are not currently available, or zero (0) if the end of the stream has been reached. + /// The buffer. + /// The buffer offset. + /// The number of bytes to read. /// The cancellation token. + /// + /// is . + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes starting + /// at the specified . + /// /// /// The stream has been disposed. /// + /// + /// The stream is in line mode (see ). + /// /// /// The operation was canceled via the cancellation token. /// /// /// An I/O error occurred. /// - internal bool ReadLine (out byte[] buffer, out int offset, out int count, CancellationToken cancellationToken) + public override async Task ReadAsync (byte[] buffer, int offset, int count, CancellationToken cancellationToken) { CheckDisposed (); + ValidateArguments (buffer, offset, count); + + if (Mode != Pop3StreamMode.Data) + throw new InvalidOperationException (); + + if (IsEndOfData || count == 0) + return 0; + + int endIndex = offset + count; + int index = offset; + int inputLeft; + + do { + inputLeft = inputEnd - inputIndex; + + // we need at least 3 bytes: ".\r\n" + if (inputLeft < 3 && (midline || NeedInput (inputIndex, inputLeft))) { + if (index > offset) + break; + + await ReadAheadAsync (cancellationToken).ConfigureAwait (false); + } + + Read (buffer, ref index, endIndex); + } while (index < endIndex && !IsEndOfData); + + return index - offset; + } + + bool TryReadLine (ByteArrayBuilder builder) + { unsafe { fixed (byte* inbuf = input) { byte* start, inptr, inend; - - if (inputIndex == inputEnd) - ReadAhead (cancellationToken); - - offset = inputIndex; - buffer = input; + int offset = inputIndex; + int count; start = inbuf + inputIndex; inend = inbuf + inputEnd; @@ -589,6 +625,7 @@ internal bool ReadLine (out byte[] buffer, out int offset, out int count, Cancel count = (int) (inptr - start); if (inptr == inend) { + builder.Append (input, offset, count); midline = true; return false; } @@ -598,11 +635,164 @@ internal bool ReadLine (out byte[] buffer, out int offset, out int count, Cancel inputIndex++; count++; + builder.Append (input, offset, count); + return true; } } } + /// + /// Reads a single line of input from the stream. + /// + /// + /// This method should be called in a loop until it returns . + /// + /// , if reading the line is complete, otherwise. + /// The output buffer to write the line data into. + /// The cancellation token. + /// + /// The stream has been disposed. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + internal bool ReadLine (ByteArrayBuilder builder, CancellationToken cancellationToken) + { + CheckDisposed (); + + if (inputIndex == inputEnd) + ReadAhead (cancellationToken); + + return TryReadLine (builder); + } + + /// + /// Asynchronously reads a single line of input from the stream. + /// + /// + /// This method should be called in a loop until it returns . + /// + /// , if reading the line is complete, otherwise. + /// The output buffer to write the line data into. + /// The cancellation token. + /// + /// The stream has been disposed. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + internal async Task ReadLineAsync (ByteArrayBuilder builder, CancellationToken cancellationToken) + { + CheckDisposed (); + + if (inputIndex == inputEnd) + await ReadAheadAsync (cancellationToken).ConfigureAwait (false); + + return TryReadLine (builder); + } + + unsafe bool TryQueueCommand (Encoder encoder, string command, ref int index) + { + fixed (char* cmd = command) { + int outputLeft = output.Length - outputIndex; + int charCount = command.Length - index; + char* chars = cmd + index; + + var needed = encoder.GetByteCount (chars, charCount, true); + + if (needed > output.Length) { + // If the command we are trying to queue is larger than the output buffer and we + // already have some commands queued in the output buffer, then flush the queue + // before queuing this command. + if (outputIndex > 0) + return false; + } else if (needed > outputLeft && index == 0) { + // If we are trying to queue a new command (index == 0) and we need more space than + // what remains in the output buffer, then flush the output buffer before queueing + // the new command. Some servers do not handle receiving partial commands well. + return false; + } + + fixed (byte* outbuf = output) { + byte* outptr = outbuf + outputIndex; + + encoder.Convert (chars, charCount, outptr, outputLeft, true, out int charsUsed, out int bytesUsed, out bool completed); + outputIndex += bytesUsed; + index += charsUsed; + + return completed; + } + } + } + + /// + /// Queue a command to the POP3 server. + /// + /// + /// Queues a command to the POP3 server. + /// + /// The character encoding. + /// The command. + /// The cancellation token. + /// + /// The stream has been disposed. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public void QueueCommand (Encoding encoding, string command, CancellationToken cancellationToken) + { + var encoder = encoding.GetEncoder (); + int index = 0; + + while (!TryQueueCommand (encoder, command, ref index)) + Flush (cancellationToken); + } + + /// + /// Asynchronously queue a command to the POP3 server. + /// + /// + /// Asynchronously queues a command to the POP3 server. + /// + /// The character encoding. + /// The command. + /// The cancellation token. + /// + /// The stream has been disposed. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public async Task QueueCommandAsync (Encoding encoding, string command, CancellationToken cancellationToken) + { + var encoder = encoding.GetEncoder (); + int index = 0; + + while (!TryQueueCommand (encoder, command, ref index)) + await FlushAsync (cancellationToken).ConfigureAwait (false); + } + + void OnWriteException (Exception ex, CancellationToken cancellationToken) + { + IsConnected = false; + if (ex is not OperationCanceledException) + cancellationToken.ThrowIfCancellationRequested (); + } + /// /// Writes a sequence of bytes to the stream and advances the current /// position within this stream by the number of bytes written. @@ -616,12 +806,12 @@ internal bool ReadLine (out byte[] buffer, out int offset, out int count, Cancel /// The number of bytes to write. /// The cancellation token. /// - /// is null. + /// is . /// /// /// is less than zero or greater than the length of . /// -or- - /// The is not large enough to contain bytes strting + /// The is not large enough to contain bytes starting /// at the specified . /// /// @@ -643,6 +833,7 @@ public void Write (byte[] buffer, int offset, int count, CancellationToken cance ValidateArguments (buffer, offset, count); try { + var network = NetworkStream.Get (Stream); int index = offset; int left = count; @@ -659,7 +850,7 @@ public void Write (byte[] buffer, int offset, int count, CancellationToken cance if (outputIndex == BlockSize) { // flush the output buffer - Poll (SelectMode.SelectWrite, cancellationToken); + network?.Poll (SelectMode.SelectWrite, cancellationToken); Stream.Write (output, 0, BlockSize); logger.LogClient (output, 0, BlockSize); outputIndex = 0; @@ -668,7 +859,7 @@ public void Write (byte[] buffer, int offset, int count, CancellationToken cance if (outputIndex == 0) { // write blocks of data to the stream without buffering while (left >= BlockSize) { - Poll (SelectMode.SelectWrite, cancellationToken); + network?.Poll (SelectMode.SelectWrite, cancellationToken); Stream.Write (buffer, index, BlockSize); logger.LogClient (buffer, index, BlockSize); index += BlockSize; @@ -676,8 +867,8 @@ public void Write (byte[] buffer, int offset, int count, CancellationToken cance } } } - } catch { - IsConnected = false; + } catch (Exception ex) { + OnWriteException (ex, cancellationToken); throw; } @@ -696,12 +887,12 @@ public void Write (byte[] buffer, int offset, int count, CancellationToken cance /// The offset of the first byte to write. /// The number of bytes to write. /// - /// is null. + /// is . /// /// /// is less than zero or greater than the length of . /// -or- - /// The is not large enough to contain bytes strting + /// The is not large enough to contain bytes starting /// at the specified . /// /// @@ -718,6 +909,86 @@ public override void Write (byte[] buffer, int offset, int count) Write (buffer, offset, count, CancellationToken.None); } + /// + /// Writes a sequence of bytes to the stream and advances the current + /// position within this stream by the number of bytes written. + /// + /// + /// Writes a sequence of bytes to the stream and advances the current + /// position within this stream by the number of bytes written. + /// + /// A task that represents the asynchronous write operation. + /// The buffer to write. + /// The offset of the first byte to write. + /// The number of bytes to write. + /// The cancellation token. + /// + /// is . + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes starting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support writing. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public override async Task WriteAsync (byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + CheckDisposed (); + + ValidateArguments (buffer, offset, count); + + try { + int index = offset; + int left = count; + + while (left > 0) { + int n = Math.Min (BlockSize - outputIndex, left); + + if (outputIndex > 0 || n < BlockSize) { + // append the data to the output buffer + Buffer.BlockCopy (buffer, index, output, outputIndex, n); + outputIndex += n; + index += n; + left -= n; + } + + if (outputIndex == BlockSize) { + // flush the output buffer + await Stream.WriteAsync (output, 0, BlockSize, cancellationToken).ConfigureAwait (false); + logger.LogClient (output, 0, BlockSize); + outputIndex = 0; + } + + if (outputIndex == 0) { + // write blocks of data to the stream without buffering + while (left >= BlockSize) { + await Stream.WriteAsync (buffer, index, BlockSize, cancellationToken).ConfigureAwait (false); + logger.LogClient (buffer, index, BlockSize); + index += BlockSize; + left -= BlockSize; + } + } + } + } catch (Exception ex) { + OnWriteException (ex, cancellationToken); + throw; + } + + IsEndOfData = false; + } + /// /// Clears all buffers for this stream and causes any buffered data to be written /// to the underlying device. @@ -747,13 +1018,16 @@ public void Flush (CancellationToken cancellationToken) return; try { - Poll (SelectMode.SelectWrite, cancellationToken); + var network = NetworkStream.Get (Stream); + + network?.Poll (SelectMode.SelectWrite, cancellationToken); Stream.Write (output, 0, outputIndex); Stream.Flush (); + logger.LogClient (output, 0, outputIndex); outputIndex = 0; - } catch { - IsConnected = false; + } catch (Exception ex) { + OnWriteException (ex, cancellationToken); throw; } } @@ -780,6 +1054,47 @@ public override void Flush () Flush (CancellationToken.None); } + /// + /// Clears all buffers for this stream and causes any buffered data to be written + /// to the underlying device. + /// + /// + /// Clears all buffers for this stream and causes any buffered data to be written + /// to the underlying device. + /// + /// A task that represents the asynchronous flush operation. + /// The cancellation token. + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support writing. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public override async Task FlushAsync (CancellationToken cancellationToken) + { + CheckDisposed (); + + if (outputIndex == 0) + return; + + try { + await Stream.WriteAsync (output, 0, outputIndex, cancellationToken).ConfigureAwait (false); + await Stream.FlushAsync (cancellationToken).ConfigureAwait (false); + + logger.LogClient (output, 0, outputIndex); + outputIndex = 0; + } catch (Exception ex) { + OnWriteException (ex, cancellationToken); + throw; + } + } + /// /// Sets the position within the current stream. /// @@ -810,8 +1125,8 @@ public override void SetLength (long value) /// Releases the unmanaged resources used by the and /// optionally releases the managed resources. /// - /// true to release both managed and unmanaged resources; - /// false to release only the unmanaged resources. + /// to release both managed and unmanaged resources; + /// to release only the unmanaged resources. protected override void Dispose (bool disposing) { if (disposing && !disposed) { diff --git a/MailKit/Net/Proxy/HttpProxyClient.cs b/MailKit/Net/Proxy/HttpProxyClient.cs new file mode 100644 index 0000000000..6b51748486 --- /dev/null +++ b/MailKit/Net/Proxy/HttpProxyClient.cs @@ -0,0 +1,272 @@ +// +// HttpProxyClient.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Net; +using System.Text; +using System.Threading; +using System.Globalization; +using System.Threading.Tasks; + +namespace MailKit.Net.Proxy +{ + /// + /// An HTTP proxy client. + /// + /// + /// An HTTP proxy client. + /// + public class HttpProxyClient : ProxyClient + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The host name of the proxy server. + /// The proxy server port. + /// + /// is . + /// + /// + /// is not between 1 and 65535. + /// + /// + /// The is a zero-length string. + /// -or- + /// The length of is greater than 255 characters. + /// + public HttpProxyClient (string host, int port) : base (host, port) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The host name of the proxy server. + /// The proxy server port. + /// The credentials to use to authenticate with the proxy server. + /// + /// is . + /// -or- + /// is . + /// + /// + /// is not between 1 and 65535. + /// + /// + /// The is a zero-length string. + /// -or- + /// The length of is greater than 255 characters. + /// + public HttpProxyClient (string host, int port, NetworkCredential credentials) : base (host, port, credentials) + { + } + + internal static byte[] GetConnectCommand (string host, int port, NetworkCredential? proxyCredentials) + { + var builder = new StringBuilder (); + + builder.AppendFormat (CultureInfo.InvariantCulture, "CONNECT {0}:{1} HTTP/1.1\r\n", host, port); + builder.AppendFormat (CultureInfo.InvariantCulture, "Host: {0}:{1}\r\n", host, port); + if (proxyCredentials != null) { + var token = Encoding.UTF8.GetBytes (string.Format (CultureInfo.InvariantCulture, "{0}:{1}", proxyCredentials.UserName, proxyCredentials.Password)); + var base64 = Convert.ToBase64String (token); + builder.AppendFormat (CultureInfo.InvariantCulture, "Proxy-Authorization: Basic {0}\r\n", base64); + } + builder.Append ("\r\n"); + + return Encoding.UTF8.GetBytes (builder.ToString ()); + } + + internal static bool TryConsumeHeaders (ByteArrayBuilder builder, byte c, ref bool newLine) + { + var endOfHeaders = false; + + switch ((char) c) { + case '\r': + break; + case '\n': + endOfHeaders = newLine; + newLine = true; + break; + default: + newLine = false; + break; + } + + builder.Append (c); + + return endOfHeaders; + } + + internal static void ValidateHttpResponse (string response, string host, int port) + { + // Verify that the response starts with something like "HTTP/1.1 200 ..." + if (response.Length >= 15 && response.StartsWith ("HTTP/1.", StringComparison.OrdinalIgnoreCase) && + (response[7] == '1' || response[7] == '0') && response[8] == ' ' && + response[9] == '2' && response[10] == '0' && response[11] == '0' && + response[12] == ' ') { + return; + } + + throw new ProxyProtocolException (string.Format (CultureInfo.InvariantCulture, "Failed to connect to {0}:{1}: {2}", host, port, response)); + } + + /// + /// Connect to the target host. + /// + /// + /// Connects to the target host and port through the proxy server. + /// + /// The connected network stream. + /// The host name of the target server. + /// The target server port. + /// The cancellation token. + /// + /// is . + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// A socket error occurred trying to connect to the remote host. + /// + /// + /// An I/O error occurred. + /// + public override Stream Connect (string host, int port, CancellationToken cancellationToken = default) + { + ValidateArguments (host, port); + + cancellationToken.ThrowIfCancellationRequested (); + + var command = GetConnectCommand (host, port, ProxyCredentials); + var socket = SocketUtils.Connect (ProxyHost, ProxyPort, LocalEndPoint, cancellationToken); + + try { + Send (socket, command, 0, command.Length, cancellationToken); + + using var builder = new ByteArrayBuilder (256); + var buffer = new byte[1]; + var newline = false; + + // read until we consume the end of the headers + do { + int nread = Receive (socket, buffer, 0, 1, cancellationToken); + + if (nread < 1 || TryConsumeHeaders (builder, buffer[0], ref newline)) + break; + } while (true); + + var response = builder.ToString (); + + ValidateHttpResponse (response, host, port); + return new NetworkStream (socket, true); + } catch { + if (socket.Connected) + socket.Disconnect (false); + socket.Dispose (); + throw; + } + } + + /// + /// Asynchronously connect to the target host. + /// + /// + /// Asynchronously connects to the target host and port through the proxy server. + /// + /// The connected network stream. + /// The host name of the target server. + /// The target server port. + /// The cancellation token. + /// + /// is . + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// A socket error occurred trying to connect to the remote host. + /// + /// + /// An I/O error occurred. + /// + public override async Task ConnectAsync (string host, int port, CancellationToken cancellationToken = default) + { + ValidateArguments (host, port); + + cancellationToken.ThrowIfCancellationRequested (); + + var socket = await SocketUtils.ConnectAsync (ProxyHost, ProxyPort, LocalEndPoint, cancellationToken).ConfigureAwait (false); + var command = GetConnectCommand (host, port, ProxyCredentials); + + try { + await SendAsync (socket, command, 0, command.Length, cancellationToken).ConfigureAwait (false); + + using var builder = new ByteArrayBuilder (256); + var buffer = new byte[1]; + var newline = false; + + // read until we consume the end of the headers + do { + int nread = await ReceiveAsync (socket, buffer, 0, 1, cancellationToken).ConfigureAwait (false); + + if (nread < 1 || TryConsumeHeaders (builder, buffer[0], ref newline)) + break; + } while (true); + + var response = builder.ToString (); + + ValidateHttpResponse (response, host, port); + return new NetworkStream (socket, true); + } catch { + if (socket.Connected) + socket.Disconnect (false); + socket.Dispose (); + throw; + } + } + } +} diff --git a/MailKit/Net/Proxy/HttpsProxyClient.cs b/MailKit/Net/Proxy/HttpsProxyClient.cs new file mode 100644 index 0000000000..88f871dc60 --- /dev/null +++ b/MailKit/Net/Proxy/HttpsProxyClient.cs @@ -0,0 +1,389 @@ +// +// HttpsProxyClient.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Net; +using System.Threading; +using System.Net.Security; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; + +using MailKit.Security; + +namespace MailKit.Net.Proxy { + /// + /// An HTTPS proxy client. + /// + /// + /// An HTTPS proxy client. + /// + public class HttpsProxyClient : ProxyClient + { +#if NET48_OR_GREATER || NET5_0_OR_GREATER + const SslProtocols DefaultSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13; +#else + const SslProtocols DefaultSslProtocols = SslProtocols.Tls12 | (SslProtocols) 12288; +#endif + + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The host name of the proxy server. + /// The proxy server port. + /// + /// is . + /// + /// + /// is not between 1 and 65535. + /// + /// + /// The is a zero-length string. + /// -or- + /// The length of is greater than 255 characters. + /// + public HttpsProxyClient (string host, int port) : base (host, port) + { + SslProtocols = DefaultSslProtocols; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The host name of the proxy server. + /// The proxy server port. + /// The credentials to use to authenticate with the proxy server. + /// + /// is . + /// -or- + /// is . + /// + /// + /// is not between 1 and 65535. + /// + /// + /// The is a zero-length string. + /// -or- + /// The length of is greater than 255 characters. + /// + public HttpsProxyClient (string host, int port, NetworkCredential credentials) : base (host, port, credentials) + { + SslProtocols = DefaultSslProtocols; + } + + /// + /// Gets or sets the set of enabled SSL and/or TLS protocol versions that the client is allowed to use. + /// + /// + /// Gets or sets the enabled SSL and/or TLS protocol versions that the client is allowed to use. + /// By default, MailKit initializes this value to enable only TLS v1.2 and greater. + /// TLS v1.1, TLS v1.0 and all versions of SSL are not enabled by default due to them all being + /// susceptible to security vulnerabilities such as POODLE. + /// This property should be set before calling any of the + /// Connect methods. + /// + /// The SSL and TLS protocol versions that are enabled. + public SslProtocols SslProtocols { + get; set; + } + +#if NET5_0_OR_GREATER + /// + /// Gets or sets the cipher suites allowed to be used when negotiating an SSL or TLS connection. + /// + /// + /// Specifies the cipher suites allowed to be used when negotiating an SSL or TLS connection. + /// When set to , the operating system default is used. Use extreme caution when + /// changing this setting. + /// + /// The cipher algorithms allowed for use when negotiating SSL or TLS encryption. + public CipherSuitesPolicy? SslCipherSuitesPolicy { + get; set; + } +#endif + + /// + /// Gets or sets the client SSL certificates. + /// + /// + /// Some servers may require the client SSL certificates in order + /// to allow the user to connect. + /// This property should be set before calling any of the + /// Connect methods. + /// + /// The client SSL certificates. + public X509CertificateCollection? ClientCertificates { + get; set; + } + + /// + /// Get or set whether connecting via SSL/TLS should check certificate revocation. + /// + /// + /// Gets or sets whether connecting via SSL/TLS should check certificate revocation. + /// Normally, the value of this property should be set to (the default) for security + /// reasons, but there are times when it may be necessary to set it to . + /// For example, most Certificate Authorities are probably pretty good at keeping their CRL and/or + /// OCSP servers up 24/7, but occasionally they do go down or are otherwise unreachable due to other + /// network problems between the client and the Certificate Authority. When this happens, it becomes + /// impossible to check the revocation status of one or more of the certificates in the chain + /// resulting in an being thrown in the + /// Connect method. If this becomes a problem, + /// it may become desirable to set to . + /// + /// if certificate revocation should be checked; otherwise, . + public bool CheckCertificateRevocation { + get; set; + } + + /// + /// Get or sets a callback function to validate the server certificate. + /// + /// + /// Gets or sets a callback function to validate the server certificate. + /// This property should be set before calling any of the + /// Connect methods. + /// + /// The server certificate validation callback function. + public RemoteCertificateValidationCallback? ServerCertificateValidationCallback { + get; set; + } + + // Note: This is used by SslHandshakeException to build the exception message. + SslCertificateValidationInfo? sslValidationInfo; + + bool ValidateRemoteCertificate (object sender, X509Certificate? certificate, X509Chain? chain, SslPolicyErrors sslPolicyErrors) + { + bool valid; + + sslValidationInfo?.Dispose (); + sslValidationInfo = null; + + if (ServerCertificateValidationCallback != null) { + valid = ServerCertificateValidationCallback (ProxyHost, certificate, chain, sslPolicyErrors); +#if NETFRAMEWORK + } else if (ServicePointManager.ServerCertificateValidationCallback != null) { + valid = ServicePointManager.ServerCertificateValidationCallback (ProxyHost, certificate, chain, sslPolicyErrors); +#endif + } else { + valid = sslPolicyErrors == SslPolicyErrors.None; + } + + if (!valid) { + // Note: The SslHandshakeException.Create() method will nullify this once it's done using it. + sslValidationInfo = new SslCertificateValidationInfo (ProxyHost, certificate, chain, sslPolicyErrors); + } + + return valid; + } + +#if NET5_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER + /// + /// Gets the SSL/TLS client authentication options for use with .NET5's SslStream.AuthenticateAsClient() API. + /// + /// + /// Gets the SSL/TLS client authentication options for use with .NET5's SslStream.AuthenticateAsClient() API. + /// + /// The target host that the client is connected to. + /// The remote certificate validation callback. + /// The client SSL/TLS authentication options. + SslClientAuthenticationOptions GetSslClientAuthenticationOptions (string host, RemoteCertificateValidationCallback remoteCertificateValidationCallback) + { + return new SslClientAuthenticationOptions { + CertificateRevocationCheckMode = CheckCertificateRevocation ? X509RevocationMode.Online : X509RevocationMode.NoCheck, + ApplicationProtocols = new List { SslApplicationProtocol.Http11 }, + RemoteCertificateValidationCallback = remoteCertificateValidationCallback, +#if NET5_0_OR_GREATER + CipherSuitesPolicy = SslCipherSuitesPolicy, +#endif + ClientCertificates = ClientCertificates, + EnabledSslProtocols = SslProtocols, + TargetHost = host + }; + } +#endif + + /// + /// Connect to the target host. + /// + /// + /// Connects to the target host and port through the proxy server. + /// + /// The connected network stream. + /// The host name of the target server. + /// The target server port. + /// The cancellation token. + /// + /// is . + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// A socket error occurred trying to connect to the remote host. + /// + /// + /// An I/O error occurred. + /// + public override Stream Connect (string host, int port, CancellationToken cancellationToken = default) + { + ValidateArguments (host, port); + + cancellationToken.ThrowIfCancellationRequested (); + + var socket = SocketUtils.Connect (ProxyHost, ProxyPort, LocalEndPoint, cancellationToken); + var ssl = new ExtendedSslStream (new NetworkStream (socket, true), false, ValidateRemoteCertificate); + + try { +#if NET5_0_OR_GREATER + ssl.AuthenticateAsClient (GetSslClientAuthenticationOptions (host, ValidateRemoteCertificate)); +#else + ssl.AuthenticateAsClient (host, ClientCertificates, SslProtocols, CheckCertificateRevocation); +#endif + } catch (Exception ex) { + ssl.Dispose (); + + throw SslHandshakeException.Create (ref sslValidationInfo, ex, false, "HTTP", host, port, 443, 80); + } + + var command = HttpProxyClient.GetConnectCommand (host, port, ProxyCredentials); + + try { + ssl.Write (command, 0, command.Length); + + using var builder = new ByteArrayBuilder (256); + var buffer = new byte[1]; + var newline = false; + + // read until we consume the end of the headers + do { + int nread = ssl.Read (buffer, 0, 1); + + if (nread < 1 || HttpProxyClient.TryConsumeHeaders (builder, buffer[0], ref newline)) + break; + } while (true); + + var response = builder.ToString (); + + HttpProxyClient.ValidateHttpResponse (response, host, port); + return ssl; + } catch { + ssl.Dispose (); + throw; + } + } + + /// + /// Asynchronously connect to the target host. + /// + /// + /// Asynchronously connects to the target host and port through the proxy server. + /// + /// The connected network stream. + /// The host name of the target server. + /// The target server port. + /// The cancellation token. + /// + /// is . + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// A socket error occurred trying to connect to the remote host. + /// + /// + /// An I/O error occurred. + /// + public override async Task ConnectAsync (string host, int port, CancellationToken cancellationToken = default) + { + ValidateArguments (host, port); + + cancellationToken.ThrowIfCancellationRequested (); + + var socket = await SocketUtils.ConnectAsync (ProxyHost, ProxyPort, LocalEndPoint, cancellationToken).ConfigureAwait (false); + var ssl = new ExtendedSslStream (new NetworkStream (socket, true), false, ValidateRemoteCertificate); + + try { +#if NET5_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER + await ssl.AuthenticateAsClientAsync (GetSslClientAuthenticationOptions (host, ValidateRemoteCertificate), cancellationToken).ConfigureAwait (false); +#else + await ssl.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, CheckCertificateRevocation).ConfigureAwait (false); +#endif + } catch (Exception ex) { + ssl.Dispose (); + + throw SslHandshakeException.Create (ref sslValidationInfo, ex, false, "HTTP", host, port, 443, 80); + } + + var command = HttpProxyClient.GetConnectCommand (host, port, ProxyCredentials); + + try { + await ssl.WriteAsync (command, 0, command.Length, cancellationToken).ConfigureAwait (false); + + using var builder = new ByteArrayBuilder (256); + var buffer = new byte[1]; + var newline = false; + + // read until we consume the end of the headers + do { + int nread = ssl.Read (buffer, 0, 1); + + if (HttpProxyClient.TryConsumeHeaders (builder, buffer[0], ref newline)) + break; + } while (true); + + var response = builder.ToString (); + + HttpProxyClient.ValidateHttpResponse (response, host, port); + return ssl; + } catch { + ssl.Dispose (); + throw; + } + } + } +} diff --git a/MailKit/Net/Proxy/IProxyClient.cs b/MailKit/Net/Proxy/IProxyClient.cs new file mode 100644 index 0000000000..95721b64f3 --- /dev/null +++ b/MailKit/Net/Proxy/IProxyClient.cs @@ -0,0 +1,212 @@ +// +// IProxyClient.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Net; +using System.Threading; +using System.Net.Sockets; +using System.Threading.Tasks; + +namespace MailKit.Net.Proxy +{ + /// + /// An interface for connecting to services via a proxy. + /// + /// + /// Implemented by , , + /// , and . + /// + /// + /// + /// + public interface IProxyClient + { + /// + /// Gets the proxy credentials. + /// + /// + /// Gets the credentials to use when authenticating with the proxy server. + /// + /// The proxy credentials. + NetworkCredential? ProxyCredentials { get; } + + /// + /// Get the proxy host. + /// + /// + /// Gets the host name of the proxy server. + /// + /// The host name of the proxy server. + string ProxyHost { get; } + + /// + /// Get the proxy port. + /// + /// + /// Gets the port to use when connecting to the proxy server. + /// + /// The proxy port. + int ProxyPort { get; } + + /// + /// Get or set the local IP end point to use when connecting to a remote host. + /// + /// + /// Gets or sets the local IP end point to use when connecting to a remote host. + /// + /// The local IP end point or to use the default end point. + IPEndPoint? LocalEndPoint { get; set; } + + /// + /// Connect to the target host. + /// + /// + /// Connects to the target host and port through the proxy server. + /// + /// The connected network stream. + /// The host name of the target server. + /// The target server port. + /// The cancellation token. + /// + /// is . + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// A socket error occurred trying to connect to the remote host. + /// + /// + /// An I/O error occurred. + /// + Stream Connect (string host, int port, CancellationToken cancellationToken = default); + + /// + /// Asynchronously connect to the target host. + /// + /// + /// Asynchronously connects to the target host and port through the proxy server. + /// + /// The connected network stream. + /// The host name of the target server. + /// The target server port. + /// The cancellation token. + /// + /// is . + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// A socket error occurred trying to connect to the remote host. + /// + /// + /// An I/O error occurred. + /// + Task ConnectAsync (string host, int port, CancellationToken cancellationToken = default); + + /// + /// Connect to the target host. + /// + /// + /// Connects to the target host and port through the proxy server. + /// + /// The connected network stream. + /// The host name of the target server. + /// The target server port. + /// The timeout, in milliseconds. + /// The cancellation token. + /// + /// is . + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// The operation timed out. + /// + /// + /// A socket error occurred trying to connect to the remote host. + /// + /// + /// An I/O error occurred. + /// + Stream Connect (string host, int port, int timeout, CancellationToken cancellationToken = default); + + /// + /// Asynchronously connect to the target host. + /// + /// + /// Asynchronously connects to the target host and port through the proxy server. + /// + /// The connected network stream. + /// The host name of the target server. + /// The target server port. + /// The timeout, in milliseconds. + /// The cancellation token. + /// + /// is . + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// The operation timed out. + /// + /// + /// A socket error occurred trying to connect to the remote host. + /// + /// + /// An I/O error occurred. + /// + Task ConnectAsync (string host, int port, int timeout, CancellationToken cancellationToken = default); + } +} diff --git a/MailKit/Net/Proxy/ProxyClient.cs b/MailKit/Net/Proxy/ProxyClient.cs new file mode 100644 index 0000000000..b447e71960 --- /dev/null +++ b/MailKit/Net/Proxy/ProxyClient.cs @@ -0,0 +1,466 @@ +// +// ProxyClient.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Net; +using System.Threading; +using System.Net.Sockets; +using System.Threading.Tasks; + +#if NET6_0_OR_GREATER +using System.Net.Http; +#endif + +namespace MailKit.Net.Proxy +{ + /// + /// An abstract proxy client base class. + /// + /// + /// A proxy client can be used to connect to a service through a firewall that + /// would otherwise be blocked. + /// + /// + /// + /// + public abstract class ProxyClient : IProxyClient + { +#if NET6_0_OR_GREATER + static IProxyClient? systemProxy; + + /// + /// Get a client for the default system proxy. + /// + /// + /// Gets a client for the default system proxy. + /// + /// A client for the default system proxy. + public static IProxyClient SystemProxy { + get { + systemProxy ??= new WebProxyClient (HttpClient.DefaultProxy); + + return systemProxy; + } + } +#endif + + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The host name of the proxy server. + /// The proxy server port. + /// + /// is . + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// -or- + /// The length of is greater than 255 characters. + /// + protected ProxyClient (string host, int port) + { + if (host == null) + throw new ArgumentNullException (nameof (host)); + + if (host.Length == 0 || host.Length > 255) + throw new ArgumentException ("The length of the host name must be between 0 and 256 characters.", nameof (host)); + + if (port < 0 || port > 65535) + throw new ArgumentOutOfRangeException (nameof (port)); + + ProxyHost = host; + ProxyPort = port == 0 ? 1080 : port; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The host name of the proxy server. + /// The proxy server port. + /// The credentials to use to authenticate with the proxy server. + /// + /// is . + /// -or- + /// is . + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// -or- + /// The length of is greater than 255 characters. + /// + protected ProxyClient (string host, int port, NetworkCredential credentials) : this (host, port) + { + if (credentials == null) + throw new ArgumentNullException (nameof (credentials)); + + ProxyCredentials = credentials; + } + + /// + /// Gets the proxy credentials. + /// + /// + /// Gets the credentials to use when authenticating with the proxy server. + /// + /// The proxy credentials. + public NetworkCredential? ProxyCredentials { + get; private set; + } + + /// + /// Get the proxy host. + /// + /// + /// Gets the host name of the proxy server. + /// + /// The host name of the proxy server. + public string ProxyHost { + get; private set; + } + + /// + /// Get the proxy port. + /// + /// + /// Gets the port to use when connecting to the proxy server. + /// + /// The proxy port. + public int ProxyPort { + get; private set; + } + + /// + /// Get or set the local IP end point to use when connecting to a remote host. + /// + /// + /// Gets or sets the local IP end point to use when connecting to a remote host. + /// + /// The local IP end point or to use the default end point. + public IPEndPoint? LocalEndPoint { + get; set; + } + + internal static void ValidateArguments (string host, int port) + { + if (host == null) + throw new ArgumentNullException (nameof (host)); + + if (host.Length == 0 || host.Length > 255) + throw new ArgumentException ("The length of the host name must be between 0 and 256 characters.", nameof (host)); + + if (port <= 0 || port > 65535) + throw new ArgumentOutOfRangeException (nameof (port)); + } + + static void ValidateArguments (string host, int port, int timeout) + { + ValidateArguments (host, port); + + if (timeout < -1) + throw new ArgumentOutOfRangeException (nameof (timeout)); + } + + static void AsyncOperationCompleted (object? sender, SocketAsyncEventArgs args) + { + var tcs = (TaskCompletionSource) args.UserToken!; + + if (args.SocketError == SocketError.Success) { + tcs.TrySetResult (true); + return; + } + + tcs.TrySetException (new SocketException ((int) args.SocketError)); + } + + internal static void Send (Socket socket, byte[] buffer, int offset, int length, CancellationToken cancellationToken) + { + if (cancellationToken.CanBeCanceled) { + var tcs = new TaskCompletionSource (); + + using (var registration = cancellationToken.Register (() => tcs.TrySetCanceled (), false)) { + using (var args = new SocketAsyncEventArgs ()) { + args.Completed += AsyncOperationCompleted; + args.SetBuffer (buffer, offset, length); + args.AcceptSocket = socket; + args.UserToken = tcs; + + if (!socket.SendAsync (args)) + AsyncOperationCompleted (null, args); + + tcs.Task.GetAwaiter ().GetResult (); + return; + } + } + } + + SocketUtils.Poll (socket, SelectMode.SelectWrite, cancellationToken); + + socket.Send (buffer, offset, length, SocketFlags.None); + } + + internal static async Task SendAsync (Socket socket, byte[] buffer, int offset, int length, CancellationToken cancellationToken) + { + var tcs = new TaskCompletionSource (); + + using (var registration = cancellationToken.Register (() => tcs.TrySetCanceled (), false)) { + using (var args = new SocketAsyncEventArgs ()) { + args.Completed += AsyncOperationCompleted; + args.SetBuffer (buffer, offset, length); + args.AcceptSocket = socket; + args.UserToken = tcs; + + if (!socket.SendAsync (args)) + AsyncOperationCompleted (null, args); + + await tcs.Task.ConfigureAwait (false); + } + } + } + + internal static int Receive (Socket socket, byte[] buffer, int offset, int length, CancellationToken cancellationToken) + { + if (cancellationToken.CanBeCanceled) { + var tcs = new TaskCompletionSource (); + + using (var registration = cancellationToken.Register (() => tcs.TrySetCanceled (), false)) { + using (var args = new SocketAsyncEventArgs ()) { + args.Completed += AsyncOperationCompleted; + args.SetBuffer (buffer, offset, length); + args.AcceptSocket = socket; + args.UserToken = tcs; + + if (!socket.ReceiveAsync (args)) + AsyncOperationCompleted (null, args); + + tcs.Task.GetAwaiter ().GetResult (); + + return args.BytesTransferred; + } + } + } + + SocketUtils.Poll (socket, SelectMode.SelectRead, cancellationToken); + + return socket.Receive (buffer, offset, length, SocketFlags.None); + } + + internal static async Task ReceiveAsync (Socket socket, byte[] buffer, int offset, int length, CancellationToken cancellationToken) + { + var tcs = new TaskCompletionSource (); + + using (var registration = cancellationToken.Register (() => tcs.TrySetCanceled (), false)) { + using (var args = new SocketAsyncEventArgs ()) { + args.Completed += AsyncOperationCompleted; + args.SetBuffer (buffer, offset, length); + args.AcceptSocket = socket; + args.UserToken = tcs; + + if (!socket.ReceiveAsync (args)) + AsyncOperationCompleted (null, args); + + await tcs.Task.ConfigureAwait (false); + + return args.BytesTransferred; + } + } + } + + /// + /// Connect to the target host. + /// + /// + /// Connects to the target host and port through the proxy server. + /// + /// The connected network stream. + /// The host name of the target server. + /// The target server port. + /// The cancellation token. + /// + /// is . + /// + /// + /// is not between 1 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// A socket error occurred trying to connect to the remote host. + /// + /// + /// An I/O error occurred. + /// + public abstract Stream Connect (string host, int port, CancellationToken cancellationToken = default); + + /// + /// Asynchronously connect to the target host. + /// + /// + /// Asynchronously connects to the target host and port through the proxy server. + /// + /// The connected network stream. + /// The host name of the target server. + /// The target server port. + /// The cancellation token. + /// + /// is . + /// + /// + /// is not between 1 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// A socket error occurred trying to connect to the remote host. + /// + /// + /// An I/O error occurred. + /// + public abstract Task ConnectAsync (string host, int port, CancellationToken cancellationToken = default); + + /// + /// Connect to the target host. + /// + /// + /// Connects to the target host and port through the proxy server. + /// + /// The connected network stream. + /// The host name of the target server. + /// The target server port. + /// The timeout, in milliseconds. + /// The cancellation token. + /// + /// is . + /// + /// + /// is not between 1 and 65535. + /// -or- + /// is less than -1. + /// + /// + /// The is a zero-length string. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// The operation timed out. + /// + /// + /// A socket error occurred trying to connect to the remote host. + /// + /// + /// An I/O error occurred. + /// + public virtual Stream Connect (string host, int port, int timeout, CancellationToken cancellationToken = default) + { + ValidateArguments (host, port, timeout); + + using (var ts = new CancellationTokenSource (timeout)) { + using (var linked = CancellationTokenSource.CreateLinkedTokenSource (cancellationToken, ts.Token)) { + try { + return Connect (host, port, linked.Token); + } catch (OperationCanceledException) { + if (!cancellationToken.IsCancellationRequested) + throw new TimeoutException (); + throw; + } + } + } + } + + /// + /// Asynchronously connect to the target host. + /// + /// + /// Asynchronously connects to the target host and port through the proxy server. + /// + /// The connected network stream. + /// The host name of the target server. + /// The target server port. + /// The timeout, in milliseconds. + /// The cancellation token. + /// + /// is . + /// + /// + /// is not between 1 and 65535. + /// -or- + /// is less than -1. + /// + /// + /// The is a zero-length string. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// The operation timed out. + /// + /// + /// A socket error occurred trying to connect to the remote host. + /// + /// + /// An I/O error occurred. + /// + public async virtual Task ConnectAsync (string host, int port, int timeout, CancellationToken cancellationToken = default) + { + ValidateArguments (host, port, timeout); + + using (var ts = new CancellationTokenSource (timeout)) { + using (var linked = CancellationTokenSource.CreateLinkedTokenSource (cancellationToken, ts.Token)) { + try { + return await ConnectAsync (host, port, linked.Token).ConfigureAwait (false); + } catch (OperationCanceledException) { + if (!cancellationToken.IsCancellationRequested) + throw new TimeoutException (); + throw; + } + } + } + } + } +} diff --git a/MailKit/Net/Proxy/ProxyProtocolException.cs b/MailKit/Net/Proxy/ProxyProtocolException.cs new file mode 100644 index 0000000000..f7a9b26d5e --- /dev/null +++ b/MailKit/Net/Proxy/ProxyProtocolException.cs @@ -0,0 +1,98 @@ +// +// ProxyProtocolException.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +#if SERIALIZABLE +using System.Security; +using System.Runtime.Serialization; +#endif + +namespace MailKit.Net.Proxy +{ + /// + /// A proxy protocol exception. + /// + /// + /// The exception that is thrown when there is an error communicating with a proxy server. + /// +#if SERIALIZABLE + [Serializable] +#endif + public class ProxyProtocolException : ProtocolException + { +#if SERIALIZABLE + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new from the serialized data. + /// + /// The serialization info. + /// The streaming context. + /// + /// is . + /// + [SecuritySafeCritical] + [Obsolete ("This API supports obsolete formatter-based serialization. It should not be called or extended by application code.")] + protected ProxyProtocolException (SerializationInfo info, StreamingContext context) : base (info, context) + { + } +#endif + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The error message. + /// An inner exception. + public ProxyProtocolException (string message, Exception innerException) : base (message, innerException) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The error message. + public ProxyProtocolException (string message) : base (message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + public ProxyProtocolException () + { + } + } +} diff --git a/MailKit/Net/Proxy/Socks4Client.cs b/MailKit/Net/Proxy/Socks4Client.cs new file mode 100644 index 0000000000..5c799897eb --- /dev/null +++ b/MailKit/Net/Proxy/Socks4Client.cs @@ -0,0 +1,368 @@ +// +// Socks4Client.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Net; +using System.Text; +using System.Threading; +using System.Net.Sockets; +using System.Globalization; +using System.Threading.Tasks; + +namespace MailKit.Net.Proxy +{ + /// + /// A SOCKS4 proxy client. + /// + /// + /// A SOCKS4 proxy client. + /// + /// + /// + /// + public class Socks4Client : SocksClient + { + static readonly byte[] InvalidIPAddress = { 0, 0, 0, 1 }; + + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The host name of the proxy server. + /// The proxy server port. + /// + /// is . + /// + /// + /// is not between 1 and 65535. + /// + /// + /// The is a zero-length string. + /// -or- + /// The length of is greater than 255 characters. + /// + public Socks4Client (string host, int port) : base (4, host, port) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The host name of the proxy server. + /// The proxy server port. + /// The credentials to use to authenticate with the proxy server. + /// + /// is . + /// -or- + /// is . + /// + /// + /// is not between 1 and 65535. + /// + /// + /// The is a zero-length string. + /// + public Socks4Client (string host, int port, NetworkCredential credentials) : base (4, host, port, credentials) + { + } + + /// + /// Get or set whether this is a Socks4a client. + /// + /// + /// Gets or sets whether this is a Socks4a client. + /// + /// if is is a Socks4a client; otherwise, . + protected bool IsSocks4a { + get; set; + } + + enum Socks4Command : byte + { + Connect = 0x01, + Bind = 0x02, + } + + enum Socks4Reply : byte + { + RequestGranted = 0x5a, + RequestRejected = 0x5b, + RequestFailedNoIdentd = 0x5c, + RequestFailedWrongId = 0x5d + } + + static string GetFailureReason (byte reply) + { + switch ((Socks4Reply) reply) { + case Socks4Reply.RequestRejected: return "Request rejected or failed."; + case Socks4Reply.RequestFailedNoIdentd: return "Request failed; unable to contact client machine's identd service."; + case Socks4Reply.RequestFailedWrongId: return "Request failed; client ID does not match specified username."; + default: return "Unknown error."; + } + } + + static IPAddress Resolve (string host, IPAddress[] ipAddresses) + { + for (int i = 0; i < ipAddresses.Length; i++) { + if (ipAddresses[i].AddressFamily == AddressFamily.InterNetwork) + return ipAddresses[i]; + } + + throw new ArgumentException ($"Could not resolve a suitable IPv4 address for '{host}'.", nameof (host)); + } + + static IPAddress Resolve (string host, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested (); + + var ipAddresses = Dns.GetHostAddresses (host); + + return Resolve (host, ipAddresses); + } + + static async Task ResolveAsync (string host, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested (); + +#if NET6_0_OR_GREATER + var ipAddresses = await Dns.GetHostAddressesAsync (host, cancellationToken).ConfigureAwait (false); +#else + var ipAddresses = await Dns.GetHostAddressesAsync (host).ConfigureAwait (false); +#endif + + return Resolve (host, ipAddresses); + } + + byte[] GetConnectCommand (byte[]? domain, byte[] addr, int port) + { + // +----+-----+----------+----------+----------+-------+--------------+-------+ + // |VER | CMD | DST.PORT | DST.ADDR | USERID | NULL | DST.DOMAIN | NULL | + // +----+-----+----------+----------+----------+-------+--------------+-------+ + // | 1 | 1 | 2 | 4 | VARIABLE | X'00' | VARIABLE | X'00' | + // +----+-----+----------+----------+----------+-------+--------------+-------+ + var user = ProxyCredentials != null ? Encoding.UTF8.GetBytes (ProxyCredentials.UserName) : Array.Empty (); + int bufferSize = 9 + user.Length + (domain != null ? domain.Length + 1 : 0); + var buffer = new byte[bufferSize]; + int n = 0; + + buffer[n++] = (byte) SocksVersion; + buffer[n++] = (byte) Socks4Command.Connect; + buffer[n++] = (byte) (port >> 8); + buffer[n++] = (byte) port; + Buffer.BlockCopy (addr, 0, buffer, n, 4); + n += 4; + Buffer.BlockCopy (user, 0, buffer, n, user.Length); + n += user.Length; + buffer[n++] = 0x00; + + if (domain != null) { + Buffer.BlockCopy (domain, 0, buffer, n, domain.Length); + n += domain.Length; + buffer[n++] = 0x00; + } + + return buffer; + } + + /// + /// Connect to the target host. + /// + /// + /// Connects to the target host and port through the proxy server. + /// + /// The connected network stream. + /// The host name of the proxy server. + /// The proxy server port. + /// The cancellation token. + /// + /// is . + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// A socket error occurred trying to connect to the remote host. + /// + /// + /// An I/O error occurred. + /// + public override Stream Connect (string host, int port, CancellationToken cancellationToken = default) + { + byte[]? domain = null; + byte[] addr; + + ValidateArguments (host, port); + + if (!IPAddress.TryParse (host, out var ip)) { + if (IsSocks4a) { + domain = Encoding.UTF8.GetBytes (host); + addr = InvalidIPAddress; + } else { + ip = Resolve (host, cancellationToken); + addr = ip.GetAddressBytes (); + } + } else { + if (ip.AddressFamily != AddressFamily.InterNetwork) + throw new ArgumentException ("The specified host address must be IPv4.", nameof (host)); + + addr = ip.GetAddressBytes (); + } + + cancellationToken.ThrowIfCancellationRequested (); + + var socket = SocketUtils.Connect (ProxyHost, ProxyPort, LocalEndPoint, cancellationToken); + + try { + var buffer = GetConnectCommand (domain, addr, port); + + Send (socket, buffer, 0, buffer.Length, cancellationToken); + + // +-----+-----+----------+----------+ + // | VER | REP | BND.PORT | BND.ADDR | + // +-----+-----+----------+----------+ + // | 1 | 1 | 2 | 4 | + // +-----+-----+----------+----------+ + int nread, n = 0; + + do { + if ((nread = Receive (socket, buffer, 0 + n, 8 - n, cancellationToken)) > 0) + n += nread; + } while (n < 8); + + if (buffer[1] != (byte) Socks4Reply.RequestGranted) + throw new ProxyProtocolException (string.Format (CultureInfo.InvariantCulture, "Failed to connect to {0}:{1}: {2}", host, port, GetFailureReason (buffer[1]))); + + // TODO: do we care about BND.ADDR and BND.PORT? + + return new NetworkStream (socket, true); + } catch { + if (socket.Connected) + socket.Disconnect (false); + + socket.Dispose (); + throw; + } + } + + /// + /// Asynchronously connect to the target host. + /// + /// + /// Asynchronously connects to the target host and port through the proxy server. + /// + /// The connected network stream. + /// The host name of the proxy server. + /// The proxy server port. + /// The cancellation token. + /// + /// is . + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// A socket error occurred trying to connect to the remote host. + /// + /// + /// An I/O error occurred. + /// + public override async Task ConnectAsync (string host, int port, CancellationToken cancellationToken = default) + { + byte[]? domain = null; + byte[] addr; + + ValidateArguments (host, port); + + if (!IPAddress.TryParse (host, out var ip)) { + if (IsSocks4a) { + domain = Encoding.UTF8.GetBytes (host); + addr = InvalidIPAddress; + } else { + ip = await ResolveAsync (host, cancellationToken).ConfigureAwait (false); + addr = ip.GetAddressBytes (); + } + } else { + if (ip.AddressFamily != AddressFamily.InterNetwork) + throw new ArgumentException ("The specified host address must be IPv4.", nameof (host)); + + addr = ip.GetAddressBytes (); + } + + cancellationToken.ThrowIfCancellationRequested (); + + var socket = await SocketUtils.ConnectAsync (ProxyHost, ProxyPort, LocalEndPoint, cancellationToken).ConfigureAwait (false); + + try { + var buffer = GetConnectCommand (domain, addr, port); + + await SendAsync (socket, buffer, 0, buffer.Length, cancellationToken).ConfigureAwait (false); + + // +-----+-----+----------+----------+ + // | VER | REP | BND.PORT | BND.ADDR | + // +-----+-----+----------+----------+ + // | 1 | 1 | 2 | 4 | + // +-----+-----+----------+----------+ + int nread, n = 0; + + do { + if ((nread = await ReceiveAsync (socket, buffer, 0 + n, 8 - n, cancellationToken).ConfigureAwait (false)) > 0) + n += nread; + } while (n < 8); + + if (buffer[1] != (byte) Socks4Reply.RequestGranted) + throw new ProxyProtocolException (string.Format (CultureInfo.InvariantCulture, "Failed to connect to {0}:{1}: {2}", host, port, GetFailureReason (buffer[1]))); + + // TODO: do we care about BND.ADDR and BND.PORT? + + return new NetworkStream (socket, true); + } catch { + if (socket.Connected) + socket.Disconnect (false); + + socket.Dispose (); + throw; + } + } + } +} diff --git a/MailKit/Net/Proxy/Socks4aClient.cs b/MailKit/Net/Proxy/Socks4aClient.cs new file mode 100644 index 0000000000..60447bff2c --- /dev/null +++ b/MailKit/Net/Proxy/Socks4aClient.cs @@ -0,0 +1,91 @@ +// +// Socks4aClient.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System.Net; + +namespace MailKit.Net.Proxy +{ + /// + /// A SOCKS4a proxy client. + /// + /// + /// A SOCKS4a proxy client. + /// + /// + /// + /// + public class Socks4aClient : Socks4Client + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The host name of the proxy server. + /// The proxy server port. + /// + /// is . + /// + /// + /// is not between 1 and 65535. + /// + /// + /// The is a zero-length string. + /// -or- + /// The length of is greater than 255 characters. + /// + public Socks4aClient (string host, int port) : base (host, port) + { + IsSocks4a = true; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The host name of the proxy server. + /// The proxy server port. + /// The credentials to use to authenticate with the proxy server. + /// + /// is . + /// -or- + /// is . + /// + /// + /// is not between 1 and 65535. + /// + /// + /// The is a zero-length string. + /// + public Socks4aClient (string host, int port, NetworkCredential credentials) : base (host, port, credentials) + { + IsSocks4a = true; + } + } +} diff --git a/MailKit/Net/Proxy/Socks5Client.cs b/MailKit/Net/Proxy/Socks5Client.cs new file mode 100644 index 0000000000..d113a2d3e5 --- /dev/null +++ b/MailKit/Net/Proxy/Socks5Client.cs @@ -0,0 +1,557 @@ +// +// Socks5Client.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Net; +using System.Text; +using System.Threading; +using System.Net.Sockets; +using System.Globalization; +using System.Threading.Tasks; + +using MailKit.Security; + +namespace MailKit.Net.Proxy +{ + /// + /// A SOCKS5 proxy client. + /// + /// + /// A SOCKS5 proxy client. + /// + /// + /// + /// + public class Socks5Client : SocksClient + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + /// The host name of the proxy server. + /// The proxy server port. + /// + /// is . + /// + /// + /// is not between 1 and 65535. + /// + /// + /// The is a zero-length string. + /// -or- + /// The length of is greater than 255 characters. + /// + public Socks5Client (string host, int port) : base (5, host, port) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + /// The host name of the proxy server. + /// The proxy server port. + /// The credentials to use to authenticate with the proxy server. + /// + /// is . + /// -or- + /// is . + /// + /// + /// is not between 1 and 65535. + /// + /// + /// The is a zero-length string. + /// -or- + /// The length of is greater than 255 characters. + /// + public Socks5Client (string host, int port, NetworkCredential credentials) : base (5, host, port, credentials) + { + } + + internal enum Socks5AddressType : byte + { + None = 0x00, + IPv4 = 0x01, + Domain = 0x03, + IPv6 = 0x04 + } + + enum Socks5AuthMethod : byte + { + Anonymous = 0x00, + GSSAPI = 0x01, + UserPassword = 0x02, + NotSupported = 0xff + } + + enum Socks5Command : byte + { + Connect = 0x01, + Bind = 0x02, + UdpAssociate = 0x03, + } + + internal enum Socks5Reply : byte + { + Success = 0x00, + GeneralServerFailure = 0x01, + ConnectionNotAllowed = 0x02, + NetworkUnreachable = 0x03, + HostUnreachable = 0x04, + ConnectionRefused = 0x05, + TTLExpired = 0x06, + CommandNotSupported = 0x07, + AddressTypeNotSupported = 0x08 + } + + internal static string GetFailureReason (byte reply) + { + switch ((Socks5Reply) reply) { + case Socks5Reply.GeneralServerFailure: return "General server failure."; + case Socks5Reply.ConnectionNotAllowed: return "Connection not allowed."; + case Socks5Reply.NetworkUnreachable: return "Network unreachable."; + case Socks5Reply.HostUnreachable: return "Host unreachable."; + case Socks5Reply.ConnectionRefused: return "Connection refused."; + case Socks5Reply.TTLExpired: return "TTL expired."; + case Socks5Reply.CommandNotSupported: return "Command not supported."; + case Socks5Reply.AddressTypeNotSupported: return "Address type not supported."; + default: return string.Format (CultureInfo.InvariantCulture, "Unknown error ({0}).", (int) reply); + } + } + + internal static Socks5AddressType GetAddressType (string host, out IPAddress? ip) + { + if (!IPAddress.TryParse (host, out ip)) + return Socks5AddressType.Domain; + + switch (ip.AddressFamily) { + case AddressFamily.InterNetworkV6: return Socks5AddressType.IPv6; + case AddressFamily.InterNetwork: return Socks5AddressType.IPv4; + default: throw new ArgumentException ("The host address must be an IPv4 or IPv6 address.", nameof (host)); + } + } + + void VerifySocksVersion (byte version) + { + if (version != (byte) SocksVersion) + throw new ProxyProtocolException (string.Format (CultureInfo.InvariantCulture, "Proxy server responded with unknown SOCKS version: {0}", (int) version)); + } + + byte[] GetNegotiateAuthMethodCommand (Socks5AuthMethod[] methods) + { + // +-----+----------+----------+ + // | VER | NMETHODS | METHODS | + // +-----+----------+----------+ + // | 1 | 1 | 1 to 255 | + // +-----+----------+----------+ + var buffer = new byte[2 + methods.Length]; + int n = 0; + + buffer[n++] = (byte) SocksVersion; + buffer[n++] = (byte) methods.Length; + for (int i = 0; i < methods.Length; i++) + buffer[n++] = (byte) methods[i]; + + return buffer; + } + + Socks5AuthMethod NegotiateAuthMethod (Socket socket, CancellationToken cancellationToken, params Socks5AuthMethod[] methods) + { + var buffer = GetNegotiateAuthMethodCommand (methods); + + Send (socket, buffer, 0, buffer.Length, cancellationToken); + + // +-----+--------+ + // | VER | METHOD | + // +-----+--------+ + // | 1 | 1 | + // +-----+--------+ + int nread, n = 0; + do { + if ((nread = Receive (socket, buffer, 0 + n, 2 - n, cancellationToken)) > 0) + n += nread; + } while (n < 2); + + VerifySocksVersion (buffer[0]); + + return (Socks5AuthMethod) buffer[1]; + } + + async Task NegotiateAuthMethodAsync (Socket socket, CancellationToken cancellationToken, params Socks5AuthMethod[] methods) + { + var buffer = GetNegotiateAuthMethodCommand (methods); + + await SendAsync (socket, buffer, 0, buffer.Length, cancellationToken).ConfigureAwait (false); + + // +-----+--------+ + // | VER | METHOD | + // +-----+--------+ + // | 1 | 1 | + // +-----+--------+ + int nread, n = 0; + do { + if ((nread = await ReceiveAsync (socket, buffer, 0 + n, 2 - n, cancellationToken).ConfigureAwait (false)) > 0) + n += nread; + } while (n < 2); + + VerifySocksVersion (buffer[0]); + + return (Socks5AuthMethod) buffer[1]; + } + + byte[] GetAuthenticateCommand () + { + var user = Encoding.UTF8.GetBytes (ProxyCredentials!.UserName); + + if (user.Length > 255) + throw new AuthenticationException ("User name too long."); + + var passwd = Encoding.UTF8.GetBytes (ProxyCredentials.Password); + + if (passwd.Length > 255) { + Array.Clear (passwd, 0, passwd.Length); + throw new AuthenticationException ("Password too long."); + } + + var buffer = new byte[user.Length + passwd.Length + 3]; + int n = 0; + + buffer[n++] = 1; + buffer[n++] = (byte) user.Length; + Buffer.BlockCopy (user, 0, buffer, n, user.Length); + n += user.Length; + buffer[n++] = (byte) passwd.Length; + Buffer.BlockCopy (passwd, 0, buffer, n, passwd.Length); + + Array.Clear (passwd, 0, passwd.Length); + + return buffer; + } + + void Authenticate (Socket socket, CancellationToken cancellationToken) + { + var buffer = GetAuthenticateCommand (); + + Send (socket, buffer, 0, buffer.Length, cancellationToken); + + int nread, n = 0; + + do { + if ((nread = Receive (socket, buffer, 0 + n, 2 - n, cancellationToken)) > 0) + n += nread; + } while (n < 2); + + if (buffer[1] != (byte) Socks5Reply.Success) + throw new AuthenticationException ("Failed to authenticate with SOCKS5 proxy server."); + } + + async Task AuthenticateAsync (Socket socket, CancellationToken cancellationToken) + { + var buffer = GetAuthenticateCommand (); + + await SendAsync (socket, buffer, 0, buffer.Length, cancellationToken).ConfigureAwait (false); + + int nread, n = 0; + + do { + if ((nread = await ReceiveAsync (socket, buffer, 0 + n, 2 - n, cancellationToken).ConfigureAwait (false)) > 0) + n += nread; + } while (n < 2); + + if (buffer[1] != (byte) Socks5Reply.Success) + throw new AuthenticationException ("Failed to authenticate with SOCKS5 proxy server."); + } + + byte[] GetConnectCommand (Socks5AddressType addrType, byte[]? domain, IPAddress? ip, int port, out int n) + { + // +----+-----+-------+------+----------+----------+ + // |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT | + // +----+-----+-------+------+----------+----------+ + // | 1 | 1 | X'00' | 1 | Variable | 2 | + // +----+-----+-------+------+----------+----------+ + var buffer = new byte[4 + 257 + 2]; + byte[] addr; + + n = 0; + + buffer[n++] = (byte) SocksVersion; + buffer[n++] = (byte) Socks5Command.Connect; + buffer[n++] = 0x00; + buffer[n++] = (byte) addrType; + switch (addrType) { + case Socks5AddressType.Domain: + buffer[n++] = (byte) domain!.Length; + Buffer.BlockCopy (domain, 0, buffer, n, domain.Length); + n += domain.Length; + break; + case Socks5AddressType.IPv6: + addr = ip!.GetAddressBytes (); + Buffer.BlockCopy (addr, 0, buffer, n, addr.Length); + n += 16; + break; + case Socks5AddressType.IPv4: + addr = ip!.GetAddressBytes (); + Buffer.BlockCopy (addr, 0, buffer, n, addr.Length); + n += 4; + break; + } + buffer[n++] = (byte) (port >> 8); + buffer[n++] = (byte) port; + + return buffer; + } + + int ProcessPartialConnectResponse (string host, int port, byte[] buffer) + { + VerifySocksVersion (buffer[0]); + + if (buffer[1] != (byte) Socks5Reply.Success) + throw new ProxyProtocolException (string.Format (CultureInfo.InvariantCulture, "Failed to connect to {0}:{1}: {2}", host, port, GetFailureReason (buffer[1]))); + + // +-----+-----+-------+------+----------+----------+ + // | VER | REP | RSV | ATYP | BND.ADDR | BND.PORT | + // +-----+-----+-------+------+----------+----------+ + // | 1 | 1 | X'00' | 1 | Variable | 2 | + // +-----+-----+-------+------+----------+----------+ + var addrType = (Socks5AddressType) buffer[3]; + + switch (addrType) { + case Socks5AddressType.Domain: return 4 + 1 + buffer[4] + 2; + case Socks5AddressType.IPv6: return 4 + 16 + 2; + case Socks5AddressType.IPv4: return 4 + 4 + 2; + default: throw new ProxyProtocolException ("Proxy server returned unknown address type."); + } + } + + /// + /// Connect to the target host. + /// + /// + /// Connects to the target host and port through the proxy server. + /// + /// The connected network stream. + /// The host name of the target server. + /// The target server port. + /// The cancellation token. + /// + /// is . + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// A socket error occurred trying to connect to the remote host. + /// + /// + /// An I/O error occurred. + /// + public override Stream Connect (string host, int port, CancellationToken cancellationToken = default) + { + ValidateArguments (host, port); + + cancellationToken.ThrowIfCancellationRequested (); + + var socket = SocketUtils.Connect (ProxyHost, ProxyPort, LocalEndPoint, cancellationToken); + var addrType = GetAddressType (host, out var ip); + byte[]? domain = null; + + if (addrType == Socks5AddressType.Domain) + domain = Encoding.UTF8.GetBytes (host); + + try { + Socks5AuthMethod method; + + if (ProxyCredentials != null) + method = NegotiateAuthMethod (socket, cancellationToken, Socks5AuthMethod.UserPassword, Socks5AuthMethod.Anonymous); + else + method = NegotiateAuthMethod (socket, cancellationToken, Socks5AuthMethod.Anonymous); + + switch (method) { + case Socks5AuthMethod.UserPassword: + Authenticate (socket, cancellationToken); + break; + case Socks5AuthMethod.Anonymous: + break; + default: + throw new ProxyProtocolException ("Failed to negotiate authentication method with the proxy server."); + } + + var buffer = GetConnectCommand (addrType, domain, ip, port, out int n); + + Send (socket, buffer, 0, n, cancellationToken); + + // +-----+-----+-------+------+----------+----------+ + // | VER | REP | RSV | ATYP | BND.ADDR | BND.PORT | + // +-----+-----+-------+------+----------+----------+ + // | 1 | 1 | X'00' | 1 | Variable | 2 | + // +-----+-----+-------+------+----------+----------+ + + // Note: We know we'll need at least 4 bytes of header + a minimum of 1 byte + // to determine the length of the BND.ADDR field if ATYP is a domain. + int nread, need = 5; + n = 0; + + do { + if ((nread = Receive (socket, buffer, 0 + n, need - n, cancellationToken)) > 0) + n += nread; + } while (n < need); + + need = ProcessPartialConnectResponse (host, port, buffer); + + do { + if ((nread = Receive (socket, buffer, 0 + n, need - n, cancellationToken)) > 0) + n += nread; + } while (n < need); + + // TODO: do we care about BND.ADDR and BND.PORT? + + return new NetworkStream (socket, true); + } catch { + if (socket.Connected) + socket.Disconnect (false); + + socket.Dispose (); + throw; + } + } + + /// + /// Asynchronously connect to the target host. + /// + /// + /// Asynchronously connects to the target host and port through the proxy server. + /// + /// The connected network stream. + /// The host name of the target server. + /// The target server port. + /// The cancellation token. + /// + /// is . + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// A socket error occurred trying to connect to the remote host. + /// + /// + /// An I/O error occurred. + /// + public override async Task ConnectAsync (string host, int port, CancellationToken cancellationToken = default) + { + ValidateArguments (host, port); + + cancellationToken.ThrowIfCancellationRequested (); + + var socket = await SocketUtils.ConnectAsync (ProxyHost, ProxyPort, LocalEndPoint, cancellationToken).ConfigureAwait (false); + var addrType = GetAddressType (host, out var ip); + byte[]? domain = null; + + if (addrType == Socks5AddressType.Domain) + domain = Encoding.UTF8.GetBytes (host); + + try { + Socks5AuthMethod method; + + if (ProxyCredentials != null) + method = await NegotiateAuthMethodAsync (socket, cancellationToken, Socks5AuthMethod.UserPassword, Socks5AuthMethod.Anonymous).ConfigureAwait (false); + else + method = await NegotiateAuthMethodAsync (socket, cancellationToken, Socks5AuthMethod.Anonymous).ConfigureAwait (false); + + switch (method) { + case Socks5AuthMethod.UserPassword: + await AuthenticateAsync (socket, cancellationToken).ConfigureAwait (false); + break; + case Socks5AuthMethod.Anonymous: + break; + default: + throw new ProxyProtocolException ("Failed to negotiate authentication method with the proxy server."); + } + + var buffer = GetConnectCommand (addrType, domain, ip, port, out int n); + + await SendAsync (socket, buffer, 0, n, cancellationToken).ConfigureAwait (false); + + // +-----+-----+-------+------+----------+----------+ + // | VER | REP | RSV | ATYP | BND.ADDR | BND.PORT | + // +-----+-----+-------+------+----------+----------+ + // | 1 | 1 | X'00' | 1 | Variable | 2 | + // +-----+-----+-------+------+----------+----------+ + + // Note: We know we'll need at least 4 bytes of header + a minimum of 1 byte + // to determine the length of the BND.ADDR field if ATYP is a domain. + int nread, need = 5; + n = 0; + + do { + if ((nread = await ReceiveAsync (socket, buffer, 0 + n, need - n, cancellationToken).ConfigureAwait (false)) > 0) + n += nread; + } while (n < need); + + need = ProcessPartialConnectResponse (host, port, buffer); + + do { + if ((nread = await ReceiveAsync (socket, buffer, 0 + n, need - n, cancellationToken).ConfigureAwait (false)) > 0) + n += nread; + } while (n < need); + + // TODO: do we care about BND.ADDR and BND.PORT? + + return new NetworkStream (socket, true); + } catch { + if (socket.Connected) + socket.Disconnect (false); + + socket.Dispose (); + throw; + } + } + } +} diff --git a/MailKit/Net/Proxy/SocksClient.cs b/MailKit/Net/Proxy/SocksClient.cs new file mode 100644 index 0000000000..16287d7c85 --- /dev/null +++ b/MailKit/Net/Proxy/SocksClient.cs @@ -0,0 +1,100 @@ +// +// SocksClient.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Net; + +namespace MailKit.Net.Proxy +{ + /// + /// An abstract SOCKS proxy client. + /// + /// + /// An abstract SOCKS proxy client. + /// + public abstract class SocksClient : ProxyClient + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The SOCKS protocol version. + /// The host name of the proxy server. + /// The proxy server port. + /// + /// is . + /// + /// + /// is not between 1 and 65535. + /// + /// + /// The is a zero-length string. + /// + protected SocksClient (int version, string host, int port) : base (host, port) + { + SocksVersion = version; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The SOCKS protocol version. + /// The host name of the proxy server. + /// The proxy server port. + /// The credentials to use to authenticate with the proxy server. + /// + /// is . + /// -or- + /// is . + /// + /// + /// is not between 1 and 65535. + /// + /// + /// The is a zero-length string. + /// + protected SocksClient (int version, string host, int port, NetworkCredential credentials) : base (host, port, credentials) + { + SocksVersion = version; + } + + /// + /// Get the SOCKS protocol version. + /// + /// + /// Gets the SOCKS protocol version. + /// + /// The SOCKS protocol version. + public int SocksVersion { + get; private set; + } + } +} diff --git a/MailKit/Net/Proxy/WebProxyClient.cs b/MailKit/Net/Proxy/WebProxyClient.cs new file mode 100644 index 0000000000..f191f6448d --- /dev/null +++ b/MailKit/Net/Proxy/WebProxyClient.cs @@ -0,0 +1,226 @@ +// +// WebProxyClient.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +#if NET6_0_OR_GREATER + +using System; +using System.IO; +using System.Net; +using System.Threading; +using System.Threading.Tasks; + +namespace MailKit.Net.Proxy +{ + /// + /// A proxy client that makes use of a . + /// + /// + /// A proxy client that makes use of a . + /// + internal class WebProxyClient : ProxyClient + { + readonly IWebProxy proxy; + + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The web proxy. + /// + /// is . + /// + public WebProxyClient (IWebProxy proxy) : base ("System", 0) + { + if (proxy is null) + throw new ArgumentNullException (nameof (proxy)); + + this.proxy = proxy; + } + + static Uri GetTargetUri (string host, int port) + { + string scheme; + + switch (port) { + case 25: case 465: case 587: scheme = "smtp"; break; + case 110: case 995: scheme = "pop"; break; + case 143: case 993: scheme = "imap"; break; + default: scheme = "http"; break; + } + + return new Uri ($"{scheme}://{host}:{port}"); + } + + static NetworkCredential? GetNetworkCredential (ICredentials? credentials, Uri uri) + { + if (credentials == null) + return null; + + if (credentials is NetworkCredential network) + return network; + + return credentials.GetCredential (uri, "Basic"); + } + + internal static ProxyClient GetProxyClient (Uri proxyUri, ICredentials? credentials) + { + var credential = GetNetworkCredential (credentials, proxyUri); + + if (proxyUri.Scheme.Equals ("https", StringComparison.OrdinalIgnoreCase)) { + if (credential != null) + return new HttpsProxyClient (proxyUri.Host, proxyUri.Port, credential); + + return new HttpsProxyClient (proxyUri.Host, proxyUri.Port); + } + + if (proxyUri.Scheme.Equals ("http", StringComparison.OrdinalIgnoreCase)) { + if (credential != null) + return new HttpProxyClient (proxyUri.Host, proxyUri.Port, credential); + + return new HttpProxyClient (proxyUri.Host, proxyUri.Port); + } + + if (proxyUri.Scheme.Equals ("socks4", StringComparison.OrdinalIgnoreCase)) { + if (credential != null) + return new Socks4Client (proxyUri.Host, proxyUri.Port, credential); + + return new Socks4Client (proxyUri.Host, proxyUri.Port); + } + + if (proxyUri.Scheme.Equals ("socks4a", StringComparison.OrdinalIgnoreCase)) { + if (credential != null) + return new Socks4aClient (proxyUri.Host, proxyUri.Port, credential); + + return new Socks4aClient (proxyUri.Host, proxyUri.Port); + } + + if (proxyUri.Scheme.Equals ("socks5", StringComparison.OrdinalIgnoreCase)) { + if (credential != null) + return new Socks5Client (proxyUri.Host, proxyUri.Port, credential); + + return new Socks5Client (proxyUri.Host, proxyUri.Port); + } + + throw new NotSupportedException ($"The default system proxy does not support {proxyUri.Scheme}."); + } + + /// + /// Connect to the target host. + /// + /// + /// Connects to the target host and port through the proxy server. + /// + /// The connected network stream. + /// The host name of the target server. + /// The target server port. + /// The cancellation token. + /// + /// is . + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// A socket error occurred trying to connect to the remote host. + /// + /// + /// An I/O error occurred. + /// + public override Stream Connect (string host, int port, CancellationToken cancellationToken = default) + { + ValidateArguments (host, port); + + var targetUri = GetTargetUri (host, port); + var proxyUri = proxy.GetProxy (targetUri); + + if (proxyUri is null || proxy.IsBypassed (targetUri)) { + // Note: if the proxy URI is null, then it means that the proxy should be bypassed. + var socket = SocketUtils.Connect (host, port, LocalEndPoint, cancellationToken); + return new NetworkStream (socket, true); + } + + var proxyClient = GetProxyClient (proxyUri, proxy.Credentials); + + return proxyClient.Connect (host, port, cancellationToken); + } + + /// + /// Asynchronously connect to the target host. + /// + /// + /// Asynchronously connects to the target host and port through the proxy server. + /// + /// The connected network stream. + /// The host name of the target server. + /// The target server port. + /// The cancellation token. + /// + /// is . + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// A socket error occurred trying to connect to the remote host. + /// + /// + /// An I/O error occurred. + /// + public override async Task ConnectAsync (string host, int port, CancellationToken cancellationToken = default) + { + ValidateArguments (host, port); + + var targetUri = GetTargetUri (host, port); + var proxyUri = proxy.GetProxy (targetUri); + + if (proxyUri is null) { + // Note: if the proxy URI is null, then it means that the proxy should be bypassed. + var socket = await SocketUtils.ConnectAsync (host, port, LocalEndPoint, cancellationToken).ConfigureAwait (false); + return new NetworkStream (socket, true); + } + + var proxyClient = GetProxyClient (proxyUri, proxy.Credentials); + + return await proxyClient.ConnectAsync (host, port, cancellationToken); + } + } +} + +#endif diff --git a/MailKit/Net/SelectMode.cs b/MailKit/Net/SelectMode.cs index 3b654cc931..893ea9fa0c 100644 --- a/MailKit/Net/SelectMode.cs +++ b/MailKit/Net/SelectMode.cs @@ -3,7 +3,7 @@ // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2019 Xamarin Inc. (www.xamarin.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal diff --git a/MailKit/Net/Smtp/AsyncSmtpClient.cs b/MailKit/Net/Smtp/AsyncSmtpClient.cs new file mode 100644 index 0000000000..3db702c7d9 --- /dev/null +++ b/MailKit/Net/Smtp/AsyncSmtpClient.cs @@ -0,0 +1,1258 @@ +// +// AsyncSmtpClient.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Net; +using System.Text; +using System.Threading; +using System.Diagnostics; +using System.Net.Sockets; +using System.Net.Security; +using System.Globalization; +using System.Threading.Tasks; +using System.Collections.Generic; + +using MimeKit; +using MimeKit.IO; + +using MailKit.Security; + +namespace MailKit.Net.Smtp +{ + public partial class SmtpClient + { + async Task QueueCommandAsync (SmtpCommand type, string command, CancellationToken cancellationToken) + { + await Stream!.QueueCommandAsync (command, cancellationToken).ConfigureAwait (false); + queued.Add (type); + } + + async Task FlushCommandQueueAsync (MimeMessage message, MailboxAddress sender, IList recipients, CancellationToken cancellationToken) + { + try { + // Note: Queued commands are buffered by the stream + await Stream!.FlushAsync (cancellationToken).ConfigureAwait (false); + } catch { + queued.Clear (); + throw; + } + + var responses = new List (queued.Count); + Exception? rex = null; + + // Note: We need to read all responses from the server before we can process + // them in case any of them have any errors so that we can RSET the state. + try { + for (int i = 0; i < queued.Count; i++) { + var response = await Stream.ReadResponseAsync (cancellationToken).ConfigureAwait (false); + responses.Add (response); + } + } catch (Exception ex) { + // Note: Most likely this exception is due to an unexpected disconnect. + // Usually, before an SMTP server disconnects the client, it will send an + // error code response that will be more useful to the user than an error + // stating that the server has unexpected disconnected. Save this exception + // in case the server didn't give us a response with an error code. + rex = ex; + } + + return ParseCommandQueueResponses (message, sender, recipients, responses, rex); + } + + async Task SendCommandInternalAsync (string command, CancellationToken cancellationToken) + { + try { + return await Stream!.SendCommandAsync (command, cancellationToken).ConfigureAwait (false); + } catch { + Disconnect (uri!.Host, uri.Port, GetSecureSocketOptions (uri), false); + throw; + } + } + + /// + /// Asynchronously send a custom command to the SMTP server. + /// + /// + /// Asynchronously sends a custom command to the SMTP server. + /// The command string should not include the terminating \r\n sequence. + /// + /// The command response. + /// The command. + /// The cancellation token. + /// + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The operation has been canceled. + /// + /// + /// An I/O error occurred. + /// + /// + /// An SMTP protocol exception occurred. + /// + protected Task SendCommandAsync (string command, CancellationToken cancellationToken = default) + { + if (command == null) + throw new ArgumentNullException (nameof (command)); + + CheckDisposed (); + + if (!IsConnected) + throw new ServiceNotConnectedException ("The SmtpClient must be connected before you can send commands."); + + if (!command.EndsWith ("\r\n", StringComparison.Ordinal)) + command += "\r\n"; + + return SendCommandInternalAsync (command, cancellationToken); + } + + Task SendEhloAsync (bool connecting, string helo, CancellationToken cancellationToken) + { + var command = CreateEhloCommand (helo); + + if (connecting) + return Stream!.SendCommandAsync (command, cancellationToken); + + return SendCommandInternalAsync (command, cancellationToken); + } + + async Task EhloAsync (bool connecting, CancellationToken cancellationToken) + { + var response = await SendEhloAsync (connecting, "EHLO", cancellationToken).ConfigureAwait (false); + + if (response.StatusCode != SmtpStatusCode.Ok) { + // Try sending HELO instead... + response = await SendEhloAsync (connecting, "HELO", cancellationToken).ConfigureAwait (false); + + if (response.StatusCode != SmtpStatusCode.Ok) + throw new SmtpCommandException (SmtpErrorCode.UnexpectedStatusCode, response.StatusCode, response.Response); + } else { + UpdateCapabilities (response); + } + } + + /// + /// Asynchronously authenticate using the specified SASL mechanism. + /// + /// + /// Authenticates using the specified SASL mechanism. + /// For a list of available SASL authentication mechanisms supported by the server, + /// check the property after the service has been + /// connected. + /// + /// An asynchronous task context. + /// The SASL mechanism. + /// The cancellation token. + /// + /// is . + /// + /// + /// The is not connected. + /// + /// + /// The is already authenticated. + /// + /// + /// The SMTP server does not support authentication. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Authentication using the supplied credentials has failed. + /// + /// + /// A SASL authentication error occurred. + /// + /// + /// An I/O error occurred. + /// + /// + /// The SMTP command failed. + /// + /// + /// An SMTP protocol error occurred. + /// + public override async Task AuthenticateAsync (SaslMechanism mechanism, CancellationToken cancellationToken = default) + { + ValidateArguments (mechanism); + + cancellationToken.ThrowIfCancellationRequested (); + + using var operation = StartNetworkOperation (NetworkOperationKind.Authenticate); + + try { + SaslException? saslException = null; + SmtpResponse response; + string challenge; + string command; + + // send an initial challenge if the mechanism supports it + if (mechanism.SupportsInitialResponse) { + challenge = await mechanism.ChallengeAsync (null, cancellationToken).ConfigureAwait (false); + command = string.Format ("AUTH {0} {1}\r\n", mechanism.MechanismName, challenge); + } else { + command = string.Format ("AUTH {0}\r\n", mechanism.MechanismName); + } + + detector.IsAuthenticating = true; + + try { + response = await SendCommandInternalAsync (command, cancellationToken).ConfigureAwait (false); + + if (response.StatusCode == SmtpStatusCode.AuthenticationMechanismTooWeak) + throw new AuthenticationException (response.Response); + + try { + while (response.StatusCode == SmtpStatusCode.AuthenticationChallenge) { + challenge = await mechanism.ChallengeAsync (response.Response, cancellationToken).ConfigureAwait (false); + response = await SendCommandInternalAsync (challenge + "\r\n", cancellationToken).ConfigureAwait (false); + } + + saslException = null; + } catch (SaslException ex) { + // reset the authentication state + response = await SendCommandInternalAsync ("\r\n", cancellationToken).ConfigureAwait (false); + saslException = ex; + } + } finally { + detector.IsAuthenticating = false; + } + + if (response.StatusCode == SmtpStatusCode.AuthenticationSuccessful) { + if (mechanism.NegotiatedSecurityLayer) + await EhloAsync (false, cancellationToken).ConfigureAwait (false); + authenticated = true; + OnAuthenticated (response.Response); + return; + } + + var message = string.Format (CultureInfo.InvariantCulture, "{0}: {1}", (int) response.StatusCode, response.Response); + + if (saslException != null) + throw new AuthenticationException (message, saslException); + + throw new AuthenticationException (message); + } catch (Exception ex) { + operation.SetError (ex); + throw; + } + } + + /// + /// Asynchronously authenticate using the supplied credentials. + /// + /// + /// Asynchronously authenticates using the supplied credentials. + /// If the SMTP server supports authentication, then the SASL mechanisms + /// that both the client and server support (not including any OAUTH mechanisms) + /// are tried in order of greatest security to weakest security. Once a SASL + /// authentication mechanism is found that both client and server support, the + /// credentials are used to authenticate. + /// If, on the other hand, authentication is not supported by the SMTP + /// server, then this method will throw . + /// The property can be checked for the + /// flag to make sure the + /// SMTP server supports authentication before calling this method. + /// To prevent the usage of certain authentication mechanisms, + /// simply remove them from the hash set + /// before calling this method. + /// + /// An asynchronous task context. + /// The text encoding to use for the user's credentials. + /// The user's credentials. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// The is not connected. + /// + /// + /// The is already authenticated. + /// + /// + /// The SMTP server does not support authentication. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Authentication using the supplied credentials has failed. + /// + /// + /// A SASL authentication error occurred. + /// + /// + /// An I/O error occurred. + /// + /// + /// The SMTP command failed. + /// + /// + /// An SMTP protocol error occurred. + /// + public override async Task AuthenticateAsync (Encoding encoding, ICredentials credentials, CancellationToken cancellationToken = default) + { + ValidateArguments (encoding, credentials); + + using var operation = StartNetworkOperation (NetworkOperationKind.Authenticate); + + try { + var saslUri = new Uri ($"smtp://{uri.Host}"); + AuthenticationException? authException = null; + SaslException? saslException; + SmtpResponse response; + SaslMechanism? sasl; + bool tried = false; + string challenge; + string command; + + foreach (var authmech in SaslMechanism.Rank (AuthenticationMechanisms)) { + var cred = credentials.GetCredential (uri, authmech); + + if (cred == null || (sasl = SaslMechanism.Create (authmech, encoding, cred)) == null) + continue; + + sasl.ChannelBindingContext = Stream.Stream as IChannelBindingContext; + sasl.Uri = saslUri; + + tried = true; + + cancellationToken.ThrowIfCancellationRequested (); + + // send an initial challenge if the mechanism supports it + if (sasl.SupportsInitialResponse) { + challenge = await sasl.ChallengeAsync (null, cancellationToken).ConfigureAwait (false); + command = string.Format ("AUTH {0} {1}\r\n", authmech, challenge); + } else { + command = string.Format ("AUTH {0}\r\n", authmech); + } + + detector.IsAuthenticating = true; + saslException = null; + + try { + response = await SendCommandInternalAsync (command, cancellationToken).ConfigureAwait (false); + + if (response.StatusCode == SmtpStatusCode.AuthenticationMechanismTooWeak) + continue; + + try { + while (!sasl.IsAuthenticated) { + if (response.StatusCode != SmtpStatusCode.AuthenticationChallenge) + break; + + challenge = await sasl.ChallengeAsync (response.Response, cancellationToken).ConfigureAwait (false); + response = await SendCommandInternalAsync (challenge + "\r\n", cancellationToken).ConfigureAwait (false); + } + + saslException = null; + } catch (SaslException ex) { + // reset the authentication state + response = await SendCommandInternalAsync ("\r\n", cancellationToken).ConfigureAwait (false); + saslException = ex; + } + } finally { + detector.IsAuthenticating = false; + } + + if (response.StatusCode == SmtpStatusCode.AuthenticationSuccessful) { + if (sasl.NegotiatedSecurityLayer) + await EhloAsync (false, cancellationToken).ConfigureAwait (false); + authenticated = true; + OnAuthenticated (response.Response); + return; + } + + var message = string.Format (CultureInfo.InvariantCulture, "{0}: {1}", (int) response.StatusCode, response.Response); + Exception inner; + + if (saslException != null) + inner = new SmtpCommandException (SmtpErrorCode.UnexpectedStatusCode, response.StatusCode, response.Response, saslException); + else + inner = new SmtpCommandException (SmtpErrorCode.UnexpectedStatusCode, response.StatusCode, response.Response); + + authException = new AuthenticationException (message, inner); + } + + if (tried) + throw authException ?? new AuthenticationException (); + + throw new NotSupportedException ("No compatible authentication mechanisms found."); + } catch (Exception ex) { + operation.SetError (ex); + throw; + } + } + + async Task SslHandshakeAsync (SslStream ssl, string host, CancellationToken cancellationToken) + { +#if NET5_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER + await ssl.AuthenticateAsClientAsync (GetSslClientAuthenticationOptions (host, ValidateRemoteCertificate), cancellationToken).ConfigureAwait (false); +#else + await ssl.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, CheckCertificateRevocation).ConfigureAwait (false); +#endif + } + + async Task PostConnectAsync (Stream stream, string host, int port, SecureSocketOptions options, bool starttls, CancellationToken cancellationToken) + { + clientConnectedTimestamp = Stopwatch.GetTimestamp (); + + try { + ProtocolLogger.LogConnect (uri!); + } catch { + stream.Dispose (); + secure = false; + throw; + } + + Stream = new SmtpStream (stream, ProtocolLogger); + + try { + // read the greeting + var response = await Stream.ReadResponseAsync (cancellationToken).ConfigureAwait (false); + + if (response.StatusCode != SmtpStatusCode.ServiceReady) + throw new SmtpCommandException (SmtpErrorCode.UnexpectedStatusCode, response.StatusCode, response.Response); + + // Send EHLO and get a list of supported extensions + await EhloAsync (true, cancellationToken).ConfigureAwait (false); + + if (options == SecureSocketOptions.StartTls && (capabilities & SmtpCapabilities.StartTLS) == 0) + throw new NotSupportedException ("The SMTP server does not support the STARTTLS extension."); + + if (starttls && (capabilities & SmtpCapabilities.StartTLS) != 0) { + response = await Stream.SendCommandAsync ("STARTTLS\r\n", cancellationToken).ConfigureAwait (false); + if (response.StatusCode != SmtpStatusCode.ServiceReady) + throw new SmtpCommandException (SmtpErrorCode.UnexpectedStatusCode, response.StatusCode, response.Response); + + try { + var tls = new ExtendedSslStream (stream, false, ValidateRemoteCertificate); + Stream.SetStream (tls); + + await SslHandshakeAsync (tls, host, cancellationToken).ConfigureAwait (false); + } catch (Exception ex) { + throw SslHandshakeException.Create (ref sslValidationInfo, ex, true, "SMTP", host, port, 465, 25, 587); + } + + secure = true; + + // Send EHLO again and get the new list of supported extensions + await EhloAsync (true, cancellationToken).ConfigureAwait (false); + } + + connected = true; + } catch (Exception ex) { + RecordClientDisconnected (ex); + Stream.Dispose (); + secure = false; + Stream = null; + throw; + } + + OnConnected (host, port, options); + } + + /// + /// Asynchronously establish a connection to the specified SMTP or SMTP/S server. + /// + /// + /// Establishes a connection to the specified SMTP or SMTP/S server. + /// If the has a value of 0, then the + /// parameter is used to determine the default port to + /// connect to. The default port used with + /// is 465. All other values will use a default port of 25. + /// If the has a value of + /// , then the is used + /// to determine the default security options. If the has a value + /// of 465, then the default options used will be + /// . All other values will use + /// . + /// Once a connection is established, properties such as + /// and will be + /// populated. + /// The connection established by any of the + /// Connect + /// methods may be re-used if an application wishes to send multiple messages + /// to the same SMTP server. Since connecting and authenticating can be expensive + /// operations, re-using a connection can significantly improve performance when + /// sending a large number of messages to the same SMTP server over a short + /// period of time. + /// + /// + /// + /// + /// An asynchronous task context. + /// The host name to connect to. + /// The port to connect to. If the specified port is 0, then the default port will be used. + /// The secure socket options to when connecting. + /// The cancellation token. + /// + /// is . + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The has been disposed. + /// + /// + /// The is already connected. + /// + /// + /// was set to + /// + /// and the SMTP server does not support the STARTTLS extension. + /// + /// + /// The operation was canceled. + /// + /// + /// A socket error occurred trying to connect to the remote host. + /// + /// + /// An error occurred during the SSL/TLS negotiations. + /// + /// + /// An I/O error occurred. + /// + /// + /// An SMTP command failed. + /// + /// + /// An SMTP protocol error occurred. + /// + public override async Task ConnectAsync (string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default) + { + ValidateArguments (host, port); + + capabilities = SmtpCapabilities.None; + AuthenticationMechanisms.Clear (); + MaxSize = 0; + + ComputeDefaultValues (host, ref port, ref options, out uri, out var starttls); + + using var operation = StartNetworkOperation (NetworkOperationKind.Connect); + + try { + var stream = await ConnectNetworkAsync (host, port, cancellationToken).ConfigureAwait (false); + stream.WriteTimeout = timeout; + stream.ReadTimeout = timeout; + + if (options == SecureSocketOptions.SslOnConnect) { + var ssl = new ExtendedSslStream (stream, false, ValidateRemoteCertificate); + + try { + await SslHandshakeAsync (ssl, host, cancellationToken).ConfigureAwait (false); + } catch (Exception ex) { + ssl.Dispose (); + + throw SslHandshakeException.Create (ref sslValidationInfo, ex, false, "SMTP", host, port, 465, 25, 587); + } + + secure = true; + stream = ssl; + } else { + secure = false; + } + + await PostConnectAsync (stream, host, port, options, starttls, cancellationToken).ConfigureAwait (false); + } catch (Exception ex) { + operation.SetError (ex); + throw; + } + } + + /// + /// Asynchronously establish a connection to the specified SMTP or SMTP/S server using the provided socket. + /// + /// + /// Establishes a connection to the specified SMTP or SMTP/S server using the provided socket. + /// If the has a value of + /// , then the is used + /// to determine the default security options. If the has a value + /// of 465, then the default options used will be + /// . All other values will use + /// . + /// Once a connection is established, properties such as + /// and will be + /// populated. + /// With the exception of using the to determine the + /// default to use when the value + /// is , the and + /// parameters are only used for logging purposes. + /// + /// An asynchronous task context. + /// The socket to use for the connection. + /// The host name to connect to. + /// The port to connect to. If the specified port is 0, then the default port will be used. + /// The secure socket options to when connecting. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// is not between 0 and 65535. + /// + /// + /// is not connected. + /// -or- + /// The is a zero-length string. + /// + /// + /// The has been disposed. + /// + /// + /// The is already connected. + /// + /// + /// was set to + /// + /// and the SMTP server does not support the STARTTLS extension. + /// + /// + /// The operation was canceled. + /// + /// + /// An error occurred during the SSL/TLS negotiations. + /// + /// + /// An I/O error occurred. + /// + /// + /// An SMTP command failed. + /// + /// + /// An SMTP protocol error occurred. + /// + public override Task ConnectAsync (Socket socket, string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default) + { + ValidateArguments (socket, host, port); + + return ConnectAsync (new NetworkStream (socket, true), host, port, options, cancellationToken); + } + + /// + /// Asynchronously establish a connection to the specified SMTP or SMTP/S server using the provided socket. + /// + /// + /// Establishes a connection to the specified SMTP or SMTP/S server using the provided socket. + /// If the has a value of + /// , then the is used + /// to determine the default security options. If the has a value + /// of 465, then the default options used will be + /// . All other values will use + /// . + /// Once a connection is established, properties such as + /// and will be + /// populated. + /// With the exception of using the to determine the + /// default to use when the value + /// is , the and + /// parameters are only used for logging purposes. + /// + /// An asynchronous task context. + /// The stream to use for the connection. + /// The host name to connect to. + /// The port to connect to. If the specified port is 0, then the default port will be used. + /// The secure socket options to when connecting. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The has been disposed. + /// + /// + /// The is already connected. + /// + /// + /// was set to + /// + /// and the SMTP server does not support the STARTTLS extension. + /// + /// + /// The operation was canceled. + /// + /// + /// An error occurred during the SSL/TLS negotiations. + /// + /// + /// An I/O error occurred. + /// + /// + /// An SMTP command failed. + /// + /// + /// An SMTP protocol error occurred. + /// + public override async Task ConnectAsync (Stream stream, string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default) + { + ValidateArguments (stream, host, port); + + capabilities = SmtpCapabilities.None; + AuthenticationMechanisms.Clear (); + MaxSize = 0; + + ComputeDefaultValues (host, ref port, ref options, out uri, out var starttls); + + using var operation = StartNetworkOperation (NetworkOperationKind.Connect); + + try { + Stream network; + + if (options == SecureSocketOptions.SslOnConnect) { + var ssl = new ExtendedSslStream (stream, false, ValidateRemoteCertificate); + + try { + await SslHandshakeAsync (ssl, host, cancellationToken).ConfigureAwait (false); + } catch (Exception ex) { + ssl.Dispose (); + + throw SslHandshakeException.Create (ref sslValidationInfo, ex, false, "SMTP", host, port, 465, 25, 587); + } + + network = ssl; + secure = true; + } else { + network = stream; + secure = false; + } + + if (network.CanTimeout) { + network.WriteTimeout = timeout; + network.ReadTimeout = timeout; + } + + await PostConnectAsync (network, host, port, options, starttls, cancellationToken).ConfigureAwait (false); + } catch (Exception ex) { + operation.SetError (ex); + throw; + } + } + + /// + /// Asynchronously disconnect the service. + /// + /// + /// If is , a QUIT command will be issued in order to disconnect cleanly. + /// + /// + /// + /// + /// An asynchronous task context. + /// If set to , a QUIT command will be issued in order to disconnect cleanly. + /// The cancellation token. + /// + /// The has been disposed. + /// + public override async Task DisconnectAsync (bool quit, CancellationToken cancellationToken = default) + { + CheckDisposed (); + + if (!IsConnected) + return; + + if (quit) { + try { + await Stream.SendCommandAsync ("QUIT\r\n", cancellationToken).ConfigureAwait (false); + } catch (OperationCanceledException) { + } catch (SmtpProtocolException) { + } catch (SmtpCommandException) { + } catch (IOException) { + } + } + + Disconnect (uri.Host, uri.Port, GetSecureSocketOptions (uri), true); + } + + /// + /// Asynchronously ping the SMTP server to keep the connection alive. + /// + /// Mail servers, if left idle for too long, will automatically drop the connection. + /// An asynchronous task context. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The operation was canceled. + /// + /// + /// An I/O error occurred. + /// + /// + /// The SMTP command failed. + /// + /// + /// An SMTP protocol error occurred. + /// + public override async Task NoOpAsync (CancellationToken cancellationToken = default) + { + CheckDisposed (); + + if (!IsConnected) + throw new ServiceNotConnectedException ("The SmtpClient is not connected."); + + var response = await SendCommandInternalAsync ("NOOP\r\n", cancellationToken).ConfigureAwait (false); + + if (response.StatusCode != SmtpStatusCode.Ok) + throw new SmtpCommandException (SmtpErrorCode.UnexpectedStatusCode, response.StatusCode, response.Response); + } + + /// + /// Asynchronously get the size of the message. + /// + /// + /// Asynchronously calculates the size of the message in bytes. + /// This method is called by SendAsync + /// methods in the following conditions: + /// + /// The SMTP server supports the SIZE= parameter in the MAIL FROM command. + /// The parameter is non-null. + /// The SMTP server supports the CHUNKING extension. + /// + /// + /// The size of the message, in bytes. + /// The formatting options. + /// The message. + /// The cancellation token. + protected virtual async Task GetSizeAsync (FormatOptions options, MimeMessage message, CancellationToken cancellationToken) + { + using (var measure = new MeasuringStream ()) { + await message.WriteToAsync (options, measure, cancellationToken).ConfigureAwait (false); + + return measure.Length; + } + } + + async Task MailFromAsync (FormatOptions options, MimeMessage message, MailboxAddress mailbox, SmtpExtensions extensions, long size, bool pipeline, CancellationToken cancellationToken) + { + var command = CreateMailFromCommand (options, message, mailbox, extensions, size); + + if (pipeline) { + await QueueCommandAsync (SmtpCommand.MailFrom, command, cancellationToken).ConfigureAwait (false); + return; + } + + var response = await Stream!.SendCommandAsync (command, cancellationToken).ConfigureAwait (false); + + ParseMailFromResponse (message, mailbox, response); + } + + async Task RcptToAsync (FormatOptions options, MimeMessage message, MailboxAddress mailbox, bool pipeline, CancellationToken cancellationToken) + { + var command = CreateRcptToCommand (options, message, mailbox); + + if (pipeline) { + await QueueCommandAsync (SmtpCommand.RcptTo, command, cancellationToken).ConfigureAwait (false); + return false; + } + + var response = await Stream!.SendCommandAsync (command, cancellationToken).ConfigureAwait (false); + + return ParseRcptToResponse (message, mailbox, response); + } + + async Task BdatAsync (FormatOptions options, MimeMessage message, long size, CancellationToken cancellationToken, ITransferProgress? progress) + { + var command = string.Format (CultureInfo.InvariantCulture, "BDAT {0} LAST\r\n", size); + + await Stream!.QueueCommandAsync (command, cancellationToken).ConfigureAwait (false); + + if (progress != null) { + var ctx = new SendContext (progress, size); + + using (var stream = new ProgressStream (Stream, ctx.Update)) { + await message.WriteToAsync (options, stream, cancellationToken).ConfigureAwait (false); + await stream.FlushAsync (cancellationToken).ConfigureAwait (false); + } + } else { + await message.WriteToAsync (options, Stream, cancellationToken).ConfigureAwait (false); + await Stream.FlushAsync (cancellationToken).ConfigureAwait (false); + } + + var response = await Stream.ReadResponseAsync (cancellationToken).ConfigureAwait (false); + + return ParseBdatResponse (message, response); + } + + async Task MessageDataAsync (FormatOptions options, MimeMessage message, long size, CancellationToken cancellationToken, ITransferProgress? progress) + { + if (progress != null) { + var ctx = new SendContext (progress, size); + + using (var stream = new ProgressStream (Stream!, ctx.Update)) { + using (var filtered = new FilteredStream (stream)) { + filtered.Add (new SmtpDataFilter ()); + + await message.WriteToAsync (options, filtered, cancellationToken).ConfigureAwait (false); + await filtered.FlushAsync (cancellationToken).ConfigureAwait (false); + } + } + } else { + using (var filtered = new FilteredStream (Stream!)) { + filtered.Add (new SmtpDataFilter ()); + + await message.WriteToAsync (options, filtered, cancellationToken).ConfigureAwait (false); + await filtered.FlushAsync (cancellationToken).ConfigureAwait (false); + } + } + + await Stream!.WriteAsync (EndData, 0, EndData.Length, cancellationToken).ConfigureAwait (false); + await Stream.FlushAsync (cancellationToken).ConfigureAwait (false); + + var response = await Stream.ReadResponseAsync (cancellationToken).ConfigureAwait (false); + + return ParseMessageDataResponse (message, response); + } + + async Task ResetAsync (CancellationToken cancellationToken) + { + SmtpResponse response; + + try { + response = await SendCommandInternalAsync ("RSET\r\n", cancellationToken).ConfigureAwait (false); + } catch { + // Swallow RSET exceptions so that we do not obscure the exception that caused the need for the RSET command in the first place. + return; + } + + if (response.StatusCode != SmtpStatusCode.Ok) + Disconnect (uri!.Host, uri.Port, GetSecureSocketOptions (uri), false); + } + + async Task SendAsync (FormatOptions options, MimeMessage message, MailboxAddress sender, IList recipients, CancellationToken cancellationToken, ITransferProgress? progress) + { + var format = Prepare (options, message, sender, recipients, out var extensions); + var pipeline = (capabilities & SmtpCapabilities.Pipelining) != 0; + var bdat = UseBdatCommand (extensions); + long size; + + if (bdat || (Capabilities & SmtpCapabilities.Size) != 0 || progress != null) { + size = await GetSizeAsync (format, message, cancellationToken).ConfigureAwait (false); + } else { + size = -1; + } + + using var operation = StartNetworkOperation (NetworkOperationKind.Send); + + try { + // Note: if PIPELINING is supported, MailFrom() and RcptTo() will + // queue their commands instead of sending them immediately. + await MailFromAsync (format, message, sender, extensions, size, pipeline, cancellationToken).ConfigureAwait (false); + + int recipientsAccepted = 0; + for (int i = 0; i < recipients.Count; i++) { + if (await RcptToAsync (format, message, recipients[i], pipeline, cancellationToken).ConfigureAwait (false)) + recipientsAccepted++; + } + + if (queued.Count > 0) { + // Note: if PIPELINING is supported, this will flush all outstanding + // MAIL FROM and RCPT TO commands to the server and then process + // all of their responses. + var results = await FlushCommandQueueAsync (message, sender, recipients, cancellationToken).ConfigureAwait (false); + + recipientsAccepted = results.RecipientsAccepted; + + if (results.FirstException != null) + throw results.FirstException; + } + + if (recipientsAccepted == 0) { + OnNoRecipientsAccepted (message); + throw new SmtpCommandException (SmtpErrorCode.MessageNotAccepted, SmtpStatusCode.TransactionFailed, "No recipients were accepted."); + } + + if (bdat) + return await BdatAsync (format, message, size, cancellationToken, progress).ConfigureAwait (false); + + var dataResponse = await Stream.SendCommandAsync ("DATA\r\n", cancellationToken).ConfigureAwait (false); + + ParseDataResponse (dataResponse); + + return await MessageDataAsync (format, message, size, cancellationToken, progress).ConfigureAwait (false); + } catch (ServiceNotAuthenticatedException ex) { + operation.SetError (ex); + + // do not disconnect + await ResetAsync (cancellationToken).ConfigureAwait (false); + throw; + } catch (SmtpCommandException ex) { + operation.SetError (ex); + + // do not disconnect + await ResetAsync (cancellationToken).ConfigureAwait (false); + throw; + } catch (Exception ex) { + operation.SetError (ex); + + Disconnect (uri!.Host, uri.Port, GetSecureSocketOptions (uri), false); + throw; + } + } + + /// + /// Asynchronously send the specified message. + /// + /// + /// Sends the specified message. + /// The sender address is determined by checking the following + /// message headers (in order of precedence): Resent-Sender, + /// Resent-From, Sender, and From. + /// If either the Resent-Sender or Resent-From addresses are present, + /// the recipients are collected from the Resent-To, Resent-Cc, and + /// Resent-Bcc headers, otherwise the To, Cc, and Bcc headers are used. + /// + /// + /// + /// + /// The final free-form text response from the server. + /// The formatting options. + /// The message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// -or- + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// Authentication is required before sending a message. + /// + /// + /// A sender has not been specified. + /// -or- + /// No recipients have been specified. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// The operation has been canceled. + /// + /// + /// An I/O error occurred. + /// + /// + /// The SMTP command failed. + /// + /// + /// An SMTP protocol exception occurred. + /// + public override Task SendAsync (FormatOptions options, MimeMessage message, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + ValidateArguments (options, message, out var sender, out var recipients); + + return SendAsync (options, message, sender, recipients, cancellationToken, progress); + } + + /// + /// Asynchronously send the specified message using the supplied sender and recipients. + /// + /// + /// Sends the message by uploading it to an SMTP server using the supplied sender and recipients. + /// + /// The final free-form text response from the server. + /// The formatting options. + /// The message. + /// The mailbox address to use for sending the message. + /// The mailbox addresses that should receive the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is . + /// -or- + /// is . + /// -or- + /// is . + /// -or- + /// is . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// Authentication is required before sending a message. + /// + /// + /// A sender has not been specified. + /// -or- + /// No recipients have been specified. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// The operation has been canceled. + /// + /// + /// An I/O error occurred. + /// + /// + /// The SMTP command failed. + /// + /// + /// An SMTP protocol exception occurred. + /// + public override Task SendAsync (FormatOptions options, MimeMessage message, MailboxAddress sender, IEnumerable recipients, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + var rcpts = ValidateArguments (options, message, sender, recipients); + + return SendAsync (options, message, sender, rcpts, cancellationToken, progress); + } + + /// + /// Asynchronously expand a mailing address alias. + /// + /// + /// Expands a mailing address alias. + /// + /// + /// + /// + /// The expanded list of mailbox addresses. + /// The mailing address alias. + /// The cancellation token. + /// + /// is . + /// + /// + /// is an empty string. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// Authentication is required before expanding an alias. + /// + /// + /// The operation has been canceled. + /// + /// + /// An I/O error occurred. + /// + /// + /// The SMTP command failed. + /// + /// + /// An SMTP protocol exception occurred. + /// + public async Task ExpandAsync (string alias, CancellationToken cancellationToken = default) + { + var response = await SendCommandInternalAsync (CreateExpandCommand (alias), cancellationToken).ConfigureAwait (false); + + return ParseExpandResponse (response); + } + + /// + /// Asynchronously verify the existence of a mailbox address. + /// + /// + /// Verifies the existence a mailbox address with the SMTP server, returning the expanded + /// mailbox address if it exists. + /// + /// + /// + /// + /// The expanded mailbox address. + /// The mailbox address. + /// The cancellation token. + /// + /// is . + /// + /// + /// is an empty string. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// Authentication is required before verifying the existence of an address. + /// + /// + /// The operation has been canceled. + /// + /// + /// An I/O error occurred. + /// + /// + /// The SMTP command failed. + /// + /// + /// An SMTP protocol exception occurred. + /// + public async Task VerifyAsync (string address, CancellationToken cancellationToken = default) + { + var response = await SendCommandInternalAsync (CreateVerifyCommand (address), cancellationToken).ConfigureAwait (false); + + return ParseVerifyResponse (response); + } + } +} diff --git a/MailKit/Net/Smtp/ISmtpClient.cs b/MailKit/Net/Smtp/ISmtpClient.cs new file mode 100644 index 0000000000..6ae947025e --- /dev/null +++ b/MailKit/Net/Smtp/ISmtpClient.cs @@ -0,0 +1,263 @@ +// +// ISmtpClient.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System.Threading; +using System.Threading.Tasks; + +using MimeKit; + +namespace MailKit.Net.Smtp { + /// + /// An interface for an SMTP client. + /// + /// + /// Implemented by . + /// + public interface ISmtpClient : IMailTransport + { + /// + /// Get the capabilities supported by the SMTP server. + /// + /// + /// The capabilities will not be known until a successful connection has been made + /// and may change once the client is authenticated. + /// + /// + /// + /// + /// The capabilities. + /// + /// Capabilities cannot be enabled, they may only be disabled. + /// + SmtpCapabilities Capabilities { get; } + + /// + /// Get or set the local domain. + /// + /// + /// The local domain is used in the HELO or EHLO commands sent to + /// the SMTP server. If left unset, the local IP address will be + /// used instead. + /// + /// The local domain. + string? LocalDomain { get; set; } + + /// + /// Get the maximum message size supported by the server. + /// + /// + /// The maximum message size will not be known until a successful connection has + /// been made and may change once the client is authenticated. + /// This value is only relevant if the includes + /// the flag. + /// + /// + /// + /// + /// The maximum message size supported by the server. + uint MaxSize { get; } + + /// + /// Get or set whether the client should use the REQUIRETLS extension if it is available. + /// + /// + /// Gets or sets whether the client should use the REQUIRETLS extension if it is available. + /// The REQUIRETLS extension (as defined in rfc8689) is a way to ensure that every SMTP server + /// that a message passes through on its way to the recipient is required to use a TLS connection in + /// order to transfer the message to the next SMTP server. + /// This feature is only available if contains the + /// flag when sending the message. + /// + /// if the REQUIRETLS extension should be used; otherwise, . + bool RequireTLS { get; set; } + + /// + /// Get or set how much of the message to include in any failed delivery status notifications. + /// + /// + /// Gets or sets how much of the message to include in any failed delivery status notifications. + /// + /// + /// + /// + /// A value indicating how much of the message to include in a failure delivery status notification. + DeliveryStatusNotificationType DeliveryStatusNotificationType { get; set; } + + /// + /// Expand a mailing address alias. + /// + /// + /// Expands a mailing address alias. + /// + /// The expanded list of mailbox addresses. + /// The mailing address alias. + /// The cancellation token. + /// + /// is . + /// + /// + /// is an empty string. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// Authentication is required before verifying the existence of an address. + /// + /// + /// The operation has been canceled. + /// + /// + /// An I/O error occurred. + /// + /// + /// The SMTP command failed. + /// + /// + /// An SMTP protocol exception occurred. + /// + InternetAddressList Expand (string alias, CancellationToken cancellationToken = default); + + /// + /// Asynchronously expand a mailing address alias. + /// + /// + /// Expands a mailing address alias. + /// + /// The expanded list of mailbox addresses. + /// The mailing address alias. + /// The cancellation token. + /// + /// is . + /// + /// + /// is an empty string. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// Authentication is required before verifying the existence of an address. + /// + /// + /// The operation has been canceled. + /// + /// + /// An I/O error occurred. + /// + /// + /// The SMTP command failed. + /// + /// + /// An SMTP protocol exception occurred. + /// + Task ExpandAsync (string alias, CancellationToken cancellationToken = default); + + /// + /// Verify the existence of a mailbox address. + /// + /// + /// Verifies the existence a mailbox address with the SMTP server, returning the expanded + /// mailbox address if it exists. + /// + /// The expanded mailbox address. + /// The mailbox address. + /// The cancellation token. + /// + /// is . + /// + /// + /// is an empty string. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// Authentication is required before verifying the existence of an address. + /// + /// + /// The operation has been canceled. + /// + /// + /// An I/O error occurred. + /// + /// + /// The SMTP command failed. + /// + /// + /// An SMTP protocol exception occurred. + /// + MailboxAddress Verify (string address, CancellationToken cancellationToken = default); + + /// + /// Asynchronously verify the existence of a mailbox address. + /// + /// + /// Verifies the existence a mailbox address with the SMTP server, returning the expanded + /// mailbox address if it exists. + /// + /// The expanded mailbox address. + /// The mailbox address. + /// The cancellation token. + /// + /// is . + /// + /// + /// is an empty string. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// Authentication is required before verifying the existence of an address. + /// + /// + /// The operation has been canceled. + /// + /// + /// An I/O error occurred. + /// + /// + /// The SMTP command failed. + /// + /// + /// An SMTP protocol exception occurred. + /// + Task VerifyAsync (string address, CancellationToken cancellationToken = default); + } +} diff --git a/MailKit/Net/Smtp/SmtpAuthenticationSecretDetector.cs b/MailKit/Net/Smtp/SmtpAuthenticationSecretDetector.cs new file mode 100644 index 0000000000..331c1aee3f --- /dev/null +++ b/MailKit/Net/Smtp/SmtpAuthenticationSecretDetector.cs @@ -0,0 +1,145 @@ +// +// SmtpAuthenticationSecretDetector.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Collections.Generic; + +namespace MailKit.Net.Smtp { + class SmtpAuthenticationSecretDetector : IAuthenticationSecretDetector + { + static readonly IList EmptyAuthSecrets = Array.Empty(); + + enum SmtpAuthCommandState + { + Auth, + AuthMechanism, + AuthNewLine, + AuthToken, + Error + } + + SmtpAuthCommandState state; + bool isAuthenticating; + int commandIndex; + + public bool IsAuthenticating { + get { return isAuthenticating; } + set { + state = SmtpAuthCommandState.Auth; + isAuthenticating = value; + commandIndex = 0; + } + } + + bool SkipCommand (string command, byte[] buffer, ref int index, int endIndex) + { + while (index < endIndex && commandIndex < command.Length) { + if (buffer[index] != (byte) command[commandIndex]) { + state = SmtpAuthCommandState.Error; + break; + } + + commandIndex++; + index++; + } + + return commandIndex == command.Length; + } + + public IList DetectSecrets (byte[] buffer, int offset, int count) + { + if (!IsAuthenticating || state == SmtpAuthCommandState.Error || count == 0) + return EmptyAuthSecrets; + + int endIndex = offset + count; + int index = offset; + + if (state == SmtpAuthCommandState.Auth) { + if (SkipCommand ("AUTH ", buffer, ref index, endIndex)) + state = SmtpAuthCommandState.AuthMechanism; + + if (index >= endIndex || state == SmtpAuthCommandState.Error) + return EmptyAuthSecrets; + } + + if (state == SmtpAuthCommandState.AuthMechanism) { + while (index < endIndex && buffer[index] != (byte) ' ' && buffer[index] != (byte) '\r') + index++; + + if (index < endIndex) { + if (buffer[index] == (byte) ' ') { + state = SmtpAuthCommandState.AuthToken; + } else { + state = SmtpAuthCommandState.AuthNewLine; + } + + index++; + } + + if (index >= endIndex) + return EmptyAuthSecrets; + } + + if (state == SmtpAuthCommandState.AuthNewLine) { + if (buffer[index] == (byte) '\n') { + state = SmtpAuthCommandState.AuthToken; + index++; + } else { + state = SmtpAuthCommandState.Error; + } + + if (index >= endIndex || state == SmtpAuthCommandState.Error) + return EmptyAuthSecrets; + } + + int startIndex = index; + while (index < endIndex && buffer[index] != (byte) '\r') + index++; + + if (index < endIndex) + state = SmtpAuthCommandState.AuthNewLine; + + if (index == startIndex) + return EmptyAuthSecrets; + + var secret = new AuthenticationSecret (startIndex, index - startIndex); + + if (state == SmtpAuthCommandState.AuthNewLine) { + index++; + + if (index < endIndex) { + if (buffer[index] == (byte) '\n') { + state = SmtpAuthCommandState.AuthToken; + } else { + state = SmtpAuthCommandState.Error; + } + } + } + + return new AuthenticationSecret[] { secret }; + } + } +} diff --git a/MailKit/Net/Smtp/SmtpCapabilities.cs b/MailKit/Net/Smtp/SmtpCapabilities.cs index 5c33145691..00d788672d 100644 --- a/MailKit/Net/Smtp/SmtpCapabilities.cs +++ b/MailKit/Net/Smtp/SmtpCapabilities.cs @@ -1,9 +1,9 @@ -// +// // SmtpCapabilities.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -38,7 +38,7 @@ namespace MailKit.Net.Smtp { /// /// [Flags] - public enum SmtpCapabilities { + public enum SmtpCapabilities : uint { /// /// The server does not support any additional extensions. /// @@ -48,59 +48,64 @@ public enum SmtpCapabilities { /// The server supports the SIZE extension /// and may have a maximum message size limitation (see ). /// - Size = (1 << 0), + Size = 1 << 0, /// /// The server supports the DSN extension, /// allowing clients to specify which (if any) recipients they would like to receive delivery /// notifications for. /// - Dsn = (1 << 1), + Dsn = 1 << 1, /// /// The server supports the ENHANCEDSTATUSCODES /// extension. /// - EnhancedStatusCodes = (1 << 2), + EnhancedStatusCodes = 1 << 2, /// /// The server supports the AUTH extension, /// allowing clients to authenticate via supported SASL mechanisms. /// - Authentication = (1 << 3), + Authentication = 1 << 3, /// /// The server supports the 8BITMIME extension, /// allowing clients to send messages using the "8bit" Content-Transfer-Encoding. /// - EightBitMime = (1 << 4), + EightBitMime = 1 << 4, /// /// The server supports the PIPELINING extension, /// allowing clients to send multiple commands at once in order to reduce round-trip latency. /// - Pipelining = (1 << 5), + Pipelining = 1 << 5, /// /// The server supports the BINARYMIME extension. /// - BinaryMime = (1 << 6), + BinaryMime = 1 << 6, /// /// The server supports the CHUNKING extension, /// allowing clients to upload messages in chunks. /// - Chunking = (1 << 7), + Chunking = 1 << 7, /// /// The server supports the STARTTLS extension, /// allowing clients to switch to an encrypted SSL/TLS connection after connecting. /// - StartTLS = (1 << 8), + StartTLS = 1 << 8, /// /// The server supports the SMTPUTF8 extension. /// - UTF8 = (1 << 9), + UTF8 = 1 << 9, + + /// + /// The server supports the REQUIRETLS extension. + /// + RequireTLS = 1 << 10, } } diff --git a/MailKit/Net/Smtp/SmtpClient.cs b/MailKit/Net/Smtp/SmtpClient.cs index 6322dce15b..f3df332c55 100644 --- a/MailKit/Net/Smtp/SmtpClient.cs +++ b/MailKit/Net/Smtp/SmtpClient.cs @@ -1,9 +1,9 @@ -// +// // SmtpClient.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -29,28 +29,30 @@ using System.Net; using System.Linq; using System.Text; +using System.Buffers; using System.Threading; +using System.Diagnostics; +using System.Net.Sockets; +using System.Net.Security; using System.Globalization; using System.Collections.Generic; +#if NET6_0_OR_GREATER +using System.Diagnostics.Metrics; +#endif +using System.Net.NetworkInformation; +using System.Security.Authentication; +using System.Runtime.CompilerServices; +using System.Diagnostics.CodeAnalysis; +using System.Security.Cryptography.X509Certificates; using MimeKit; using MimeKit.IO; -using System.Threading.Tasks; - -#if NETFX_CORE -using Windows.Networking; -using Windows.Networking.Sockets; -using Windows.Storage.Streams; -using Socket = Windows.Networking.Sockets.StreamSocket; -using Encoding = Portable.Text.Encoding; -#else -using System.Net.Sockets; -using System.Net.Security; -using System.Security.Cryptography.X509Certificates; -#endif +using MimeKit.Cryptography; using MailKit.Security; +using AuthenticationException = MailKit.Security.AuthenticationException; + namespace MailKit.Net.Smtp { /// /// An SMTP client that can be used to send email messages. @@ -67,11 +69,13 @@ namespace MailKit.Net.Smtp { /// large number of messages to the same SMTP server over a short period of time. /// /// - /// + /// /// - public class SmtpClient : MailTransport + public partial class SmtpClient : MailTransport, ISmtpClient { - static readonly byte[] EndData = Encoding.ASCII.GetBytes ("\r\n.\r\n"); + static readonly byte[] EndData = Encoding.ASCII.GetBytes (".\r\n"); + static readonly char[] NewLineCharacters = { '\r', '\n' }; + internal static readonly string DefaultLocalDomain; const int MaxLineLength = 998; enum SmtpCommand { @@ -79,15 +83,52 @@ enum SmtpCommand { RcptTo } - readonly HashSet authenticationMechanisms = new HashSet (); + readonly HashSet authenticationMechanisms = new HashSet (StringComparer.Ordinal); + readonly SmtpAuthenticationSecretDetector detector = new SmtpAuthenticationSecretDetector (); readonly List queued = new List (); + SslCertificateValidationInfo? sslValidationInfo; +#if NET6_0_OR_GREATER + readonly ClientMetrics? metrics; +#endif + long clientConnectedTimestamp; SmtpCapabilities capabilities; - int timeout = 100000; + int timeout = 2 * 60 * 1000; bool authenticated; bool connected; bool disposed; bool secure; - string host; + Uri? uri; + + internal static string? GetSafeHostName (string? hostName) + { + var idn = new IdnMapping (); + + if (!string.IsNullOrEmpty (hostName)) { + hostName = hostName!.Replace ('_', '-'); + + try { + return idn.GetAscii (hostName); + } catch { + // This can happen if the hostName contains illegal unicode characters. + var ascii = new StringBuilder (); + for (int i = 0; i < hostName.Length; i++) { + if (hostName[i] <= 0x7F) + ascii.Append (hostName[i]); + } + + return ascii.Length > 0 ? ascii.ToString () : null; + } + } else { + return null; + } + } + + static SmtpClient () + { + var hostName = GetSafeHostName (IPGlobalProperties.GetIPGlobalProperties ().HostName); + + DefaultLocalDomain = hostName ?? "localhost"; + } /// /// Initializes a new instance of the class. @@ -100,7 +141,7 @@ enum SmtpCommand { /// Authenticate methods. /// /// - /// + /// /// public SmtpClient () : this (new NullProtocolLogger ()) { @@ -118,30 +159,53 @@ public SmtpClient () : this (new NullProtocolLogger ()) /// /// The protocol logger. /// - /// is null. + /// is . /// /// /// /// public SmtpClient (IProtocolLogger protocolLogger) : base (protocolLogger) { + protocolLogger.AuthenticationSecretDetector = detector; + +#if NET6_0_OR_GREATER + // Use the globally configured SmtpClient metrics. + metrics = Telemetry.SmtpClient.Metrics; +#endif } +#if NET8_0_OR_GREATER /// - /// Get or set whether or not the capabilities should be re-queried after authenticating. + /// Initializes a new instance of the class. /// /// - /// Certain servers do not properly follow the specification and break if an EHLO - /// command is sent after authenticating, causing the sending of mail to fail with various - /// errors, typically suggesting an invalid state. - /// Since the SMTP SASL specifications specifically state that clients should re-query - /// the capabilities after successfully authenticating, the default is true. + /// Before you can send messages with the , you must first call one of + /// the Connect methods. + /// Depending on whether the SMTP server requires authenticating or not, you may also need to + /// authenticate using one of the + /// Authenticate methods. /// - /// true if the capabilities should be re-queried after authenticating; otherwise, false. - [Obsolete ("This property is no longer needed.")] - public bool QueryCapabilitiesAfterAuthenticating { - get; set; + /// The protocol logger. + /// The meter factory. + /// + /// is . + /// -or- + /// is . + /// + /// + /// + /// + public SmtpClient (IProtocolLogger protocolLogger, IMeterFactory meterFactory) : base (protocolLogger) + { + if (meterFactory == null) + throw new ArgumentNullException (nameof (meterFactory)); + + protocolLogger.AuthenticationSecretDetector = detector; + + var meter = meterFactory.Create (Telemetry.SmtpClient.MeterName, Telemetry.SmtpClient.MeterVersion); + metrics = Telemetry.SmtpClient.CreateMetrics (meter); } +#endif /// /// Get the underlying SMTP stream. @@ -150,7 +214,7 @@ public bool QueryCapabilitiesAfterAuthenticating { /// Gets the underlying SMTP stream. /// /// The SMTP stream. - SmtpStream Stream { + SmtpStream? Stream { get; set; } @@ -158,9 +222,9 @@ SmtpStream Stream { /// Gets an object that can be used to synchronize access to the SMTP server. /// /// - /// Gets an object that can be used to synchronize access to the SMTP server. - /// When using the non-Async methods from multiple threads, it is important to lock the - /// object for thread safety when using the synchronous methods. + /// Gets an object that can be used to synchronize access to the SMTP server between multiple threads. + /// When using methods from multiple threads, it is important to lock the + /// object for thread safety. /// /// The lock object. public override object SyncRoot { @@ -182,7 +246,7 @@ protected override string Protocol { /// Get the capabilities supported by the SMTP server. /// /// - /// The capabilities will not be known until a successful connection has been made + /// The capabilities will not be known until a successful connection has been made /// and may change once the client is authenticated. /// /// @@ -203,7 +267,7 @@ public SmtpCapabilities Capabilities { } /// - /// Gets or sets the local domain. + /// Get or set the local domain. /// /// /// The local domain is used in the HELO or EHLO commands sent to @@ -211,10 +275,27 @@ public SmtpCapabilities Capabilities { /// used instead. /// /// The local domain. - public string LocalDomain { + public string? LocalDomain { get; set; } + /// + /// Get whether or not the BDAT command is preferred over the DATA command. + /// + /// + /// Gets whether or not the BDAT command is preferred over the standard DATA + /// command. + /// The BDAT command is normally only used when the message being sent contains binary data + /// (e.g. one or more MIME parts contains a Content-Transfer-Encoding: binary header). This + /// option provides a way to override this behavior, forcing the to send + /// messages using the BDAT command instead of the DATA command even when it is not + /// necessary to do so. + /// + /// if the BDAT command is preferred over the DATA command; otherwise, . + protected virtual bool PreferSendAsBinaryData { + get { return false; } + } + /// /// Get the maximum message size supported by the server. /// @@ -232,6 +313,22 @@ public uint MaxSize { get; private set; } + /// + /// Get or set whether the client should use the REQUIRETLS extension if it is available. + /// + /// + /// Gets or sets whether the client should use the REQUIRETLS extension if it is available. + /// The REQUIRETLS extension (as defined in rfc8689) is a way to ensure that every SMTP server + /// that a message passes through on its way to the recipient is required to use a TLS connection in + /// order to transfer the message to the next SMTP server. + /// This feature is only available if contains the + /// flag when sending the message. + /// + /// if the REQUIRETLS extension should be used; otherwise, . + public bool RequireTLS { + get; set; + } + void CheckDisposed () { if (disposed) @@ -282,9 +379,9 @@ public override int Timeout { /// Get whether or not the client is currently connected to an SMTP server. /// /// - /// The state is set to true immediately after + /// The state is set to immediately after /// one of the Connect - /// methods succeeds and is not set back to false until either the client + /// methods succeeds and is not set back to until either the client /// is disconnected via or until an /// is thrown while attempting to read or write to /// the underlying network socket. @@ -294,7 +391,8 @@ public override int Timeout { /// /// /// - /// true if the client is connected; otherwise, false. + /// if the client is connected; otherwise, . + [MemberNotNullWhen (true, new[] { nameof (Stream), nameof (uri) })] public override bool IsConnected { get { return connected; } } @@ -305,11 +403,203 @@ public override bool IsConnected { /// /// Gets whether or not the connection is secure (typically via SSL or TLS). /// - /// true if the connection is secure; otherwise, false. + /// if the connection is secure; otherwise, . + [MemberNotNullWhen (true, new[] { nameof (Stream), nameof (uri) })] public override bool IsSecure { get { return IsConnected && secure; } } + /// + /// Get whether or not the connection is encrypted (typically via SSL or TLS). + /// + /// + /// Gets whether or not the connection is encrypted (typically via SSL or TLS). + /// + /// if the connection is encrypted; otherwise, . + public override bool IsEncrypted { + get { return IsSecure && (Stream.Stream is SslStream sslStream) && sslStream.IsEncrypted; } + } + + /// + /// Get whether or not the connection is signed (typically via SSL or TLS). + /// + /// + /// Gets whether or not the connection is signed (typically via SSL or TLS). + /// + /// if the connection is signed; otherwise, . + public override bool IsSigned { + get { return IsSecure && (Stream.Stream is SslStream sslStream) && sslStream.IsSigned; } + } + + /// + /// Get the negotiated SSL or TLS protocol version. + /// + /// + /// Gets the negotiated SSL or TLS protocol version once an SSL or TLS connection has been made. + /// + /// + /// + /// + /// The negotiated SSL or TLS protocol version. + public override SslProtocols SslProtocol { + get { + if (IsSecure && (Stream.Stream is SslStream sslStream)) + return sslStream.SslProtocol; + + return SslProtocols.None; + } + } + + /// + /// Get the negotiated SSL or TLS cipher algorithm. + /// + /// + /// Gets the negotiated SSL or TLS cipher algorithm once an SSL or TLS connection has been made. + /// + /// + /// + /// + /// The negotiated SSL or TLS cipher algorithm. +#if NET10_0_OR_GREATER + [Obsolete ("Use SslCipherSuite instead.")] +#endif + public override CipherAlgorithmType? SslCipherAlgorithm { + get { + if (IsSecure && (Stream.Stream is SslStream sslStream)) + return sslStream.CipherAlgorithm; + + return null; + } + } + + /// + /// Get the negotiated SSL or TLS cipher algorithm strength. + /// + /// + /// Gets the negotiated SSL or TLS cipher algorithm strength once an SSL or TLS connection has been made. + /// + /// + /// + /// + /// The negotiated SSL or TLS cipher algorithm strength. +#if NET10_0_OR_GREATER + [Obsolete ("Use SslCipherSuite instead.")] +#endif + public override int? SslCipherStrength { + get { + if (IsSecure && (Stream.Stream is SslStream sslStream)) + return sslStream.CipherStrength; + + return null; + } + } + +#if NET5_0_OR_GREATER + /// + /// Get the negotiated SSL or TLS cipher suite. + /// + /// + /// Gets the negotiated SSL or TLS cipher suite once an SSL or TLS connection has been made. + /// + /// The negotiated SSL or TLS cipher suite. + public override TlsCipherSuite? SslCipherSuite { + get { + if (IsSecure && (Stream.Stream is SslStream sslStream)) + return sslStream.NegotiatedCipherSuite; + + return null; + } + } +#endif + + /// + /// Get the negotiated SSL or TLS hash algorithm. + /// + /// + /// Gets the negotiated SSL or TLS hash algorithm once an SSL or TLS connection has been made. + /// + /// + /// + /// + /// The negotiated SSL or TLS hash algorithm. +#if NET10_0_OR_GREATER + [Obsolete ("Use SslCipherSuite instead.")] +#endif + public override HashAlgorithmType? SslHashAlgorithm { + get { + if (IsSecure && (Stream.Stream is SslStream sslStream)) + return sslStream.HashAlgorithm; + + return null; + } + } + + /// + /// Get the negotiated SSL or TLS hash algorithm strength. + /// + /// + /// Gets the negotiated SSL or TLS hash algorithm strength once an SSL or TLS connection has been made. + /// + /// + /// + /// + /// The negotiated SSL or TLS hash algorithm strength. +#if NET10_0_OR_GREATER + [Obsolete ("Use SslCipherSuite instead.")] +#endif + public override int? SslHashStrength { + get { + if (IsSecure && (Stream.Stream is SslStream sslStream)) + return sslStream.HashStrength; + + return null; + } + } + + /// + /// Get the negotiated SSL or TLS key exchange algorithm. + /// + /// + /// Gets the negotiated SSL or TLS key exchange algorithm once an SSL or TLS connection has been made. + /// + /// + /// + /// + /// The negotiated SSL or TLS key exchange algorithm. +#if NET10_0_OR_GREATER + [Obsolete ("Use SslCipherSuite instead.")] +#endif + public override ExchangeAlgorithmType? SslKeyExchangeAlgorithm { + get { + if (IsSecure && (Stream.Stream is SslStream sslStream)) + return sslStream.KeyExchangeAlgorithm; + + return null; + } + } + + /// + /// Get the negotiated SSL or TLS key exchange algorithm strength. + /// + /// + /// Gets the negotiated SSL or TLS key exchange algorithm strength once an SSL or TLS connection has been made. + /// + /// + /// + /// + /// The negotiated SSL or TLS key exchange algorithm strength. +#if NET10_0_OR_GREATER + [Obsolete ("Use SslCipherSuite instead.")] +#endif + public override int? SslKeyExchangeStrength { + get { + if (IsSecure && (Stream.Stream is SslStream sslStream)) + return sslStream.KeyExchangeStrength; + + return null; + } + } + /// /// Get whether or not the client is currently authenticated with the SMTP server. /// @@ -319,33 +609,44 @@ public override bool IsSecure { /// Authenticate /// methods. /// - /// true if the client is connected; otherwise, false. + /// if the client is authenticated; otherwise, . public override bool IsAuthenticated { get { return authenticated; } } -#if !NETFX_CORE - bool ValidateRemoteCertificate (object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) + NetworkOperation StartNetworkOperation (NetworkOperationKind kind) { - if (ServerCertificateValidationCallback != null) - return ServerCertificateValidationCallback (host, certificate, chain, sslPolicyErrors); - -#if !NETSTANDARD - if (ServicePointManager.ServerCertificateValidationCallback != null) - return ServicePointManager.ServerCertificateValidationCallback (host, certificate, chain, sslPolicyErrors); +#if NET6_0_OR_GREATER + return NetworkOperation.Start (kind, uri!, Telemetry.SmtpClient.ActivitySource, metrics); +#else + return NetworkOperation.Start (kind, uri!); #endif - - return DefaultServerCertificateValidationCallback (sender, certificate, chain, sslPolicyErrors); } -#endif - void QueueCommand (SmtpCommand type, string command, CancellationToken cancellationToken) + bool ValidateRemoteCertificate (object? sender, X509Certificate? certificate, X509Chain? chain, SslPolicyErrors sslPolicyErrors) { - var bytes = Encoding.UTF8.GetBytes (command + "\r\n"); + var host = uri!.Host; + bool valid; - // Note: queued commands will be buffered by the stream - Stream.Write (bytes, 0, bytes.Length, cancellationToken); - queued.Add (type); + sslValidationInfo?.Dispose (); + sslValidationInfo = null; + + if (ServerCertificateValidationCallback != null) { + valid = ServerCertificateValidationCallback (host, certificate, chain, sslPolicyErrors); +#if NETFRAMEWORK + } else if (ServicePointManager.ServerCertificateValidationCallback != null) { + valid = ServicePointManager.ServerCertificateValidationCallback (host, certificate, chain, sslPolicyErrors); +#endif + } else { + valid = DefaultServerCertificateValidationCallback (host, certificate, chain, sslPolicyErrors); + } + + if (!valid) { + // Note: The SslHandshakeException.Create() method will nullify this once it's done using it. + sslValidationInfo = new SslCertificateValidationInfo (host, certificate, chain, sslPolicyErrors); + } + + return valid; } /// @@ -361,232 +662,336 @@ protected virtual void OnNoRecipientsAccepted (MimeMessage message) { } - void FlushCommandQueue (MimeMessage message, MailboxAddress sender, IList recipients, CancellationToken cancellationToken) + void QueueCommand (SmtpCommand type, string command, CancellationToken cancellationToken) { - try { - var responses = new List (); - Exception rex = null; - int accepted = 0; - int rcpt = 0; + Stream!.QueueCommand (command, cancellationToken); + queued.Add (type); + } - // Note: queued commands are buffered by the stream - Stream.Flush (cancellationToken); + struct QueueResults + { + public readonly int RecipientsAccepted; + public readonly Exception? FirstException; - // Note: we need to read all responses from the server before we can process - // them in case any of them have any errors so that we can RSET the state. - try { - for (int i = 0; i < queued.Count; i++) - responses.Add (Stream.ReadResponse (cancellationToken)); - } catch (Exception ex) { - // Note: save this exception for later (it may be related to - // an error response for a MAIL FROM or RCPT TO command). - rex = ex; - } + public QueueResults (int recipientsAccepted, Exception? firstException) + { + RecipientsAccepted = recipientsAccepted; + FirstException = firstException; + } + } + + QueueResults ParseCommandQueueResponses (MimeMessage message, MailboxAddress sender, IList recipients, List responses, Exception? readResponseException) + { + Exception? firstException = null; + int recipientsAccepted = 0; + int rcpt = 0; + try { + // process the responses for (int i = 0; i < responses.Count; i++) { switch (queued[i]) { case SmtpCommand.MailFrom: - ProcessMailFromResponse (message, sender, responses[i]); + try { + ParseMailFromResponse (message, sender, responses[i]); + } catch (Exception ex) { + firstException ??= ex; + } break; case SmtpCommand.RcptTo: - if (ProcessRcptToResponse (message, recipients[rcpt++], responses[i])) - accepted++; + try { + if (ParseRcptToResponse (message, recipients[rcpt++], responses[i])) + recipientsAccepted++; + } catch (Exception ex) { + firstException ??= ex; + } break; } } - - if (accepted == 0) - OnNoRecipientsAccepted (message); - - if (rex != null) - throw new SmtpProtocolException ("Error reading a response from the SMTP server.", rex); } finally { queued.Clear (); } - } - - SmtpResponse SendCommand (string command, CancellationToken cancellationToken) - { - var bytes = Encoding.UTF8.GetBytes (command + "\r\n"); - - Stream.Write (bytes, 0, bytes.Length, cancellationToken); - Stream.Flush (cancellationToken); - return Stream.ReadResponse (cancellationToken); + return new QueueResults (recipientsAccepted, firstException ?? readResponseException); } - SmtpResponse SendEhlo (bool ehlo, CancellationToken cancellationToken) + QueueResults FlushCommandQueue (MimeMessage message, MailboxAddress sender, IList recipients, CancellationToken cancellationToken) { - string command = ehlo ? "EHLO " : "HELO "; - -#if !NETFX_CORE - string domain = null; - IPAddress ip = null; - - if (!string.IsNullOrEmpty (LocalDomain)) { - if (!IPAddress.TryParse (LocalDomain, out ip)) - domain = LocalDomain; - } else if (Stream.Socket != null) { - var ipEndPoint = Stream.Socket.LocalEndPoint as IPEndPoint; - - if (ipEndPoint == null) - domain = ((DnsEndPoint) Stream.Socket.LocalEndPoint).Host; - else - ip = ipEndPoint.Address; - } else { - domain = "[127.0.0.1]"; + try { + // Note: Queued commands are buffered by the stream + Stream!.Flush (cancellationToken); + } catch { + queued.Clear (); + throw; } - if (ip != null) { - if (ip.AddressFamily == AddressFamily.InterNetworkV6) - domain = "[IPv6:" + ip + "]"; - else - domain = "[" + ip + "]"; - } + var responses = new List (queued.Count); + Exception? rex = null; - command += domain; -#else - if (!string.IsNullOrEmpty (LocalDomain)) - command += LocalDomain; - else if (!string.IsNullOrEmpty (Stream.Socket.Information.LocalAddress.CanonicalName)) - command += Stream.Socket.Information.LocalAddress.CanonicalName; - else - command += "localhost.localdomain"; -#endif + // Note: We need to read all responses from the server before we can process + // them in case any of them have any errors so that we can RSET the state. + try { + for (int i = 0; i < queued.Count; i++) { + var response = Stream.ReadResponse (cancellationToken); + responses.Add (response); + } + } catch (Exception ex) { + // Note: Most likely this exception is due to an unexpected disconnect. + // Usually, before an SMTP server disconnects the client, it will send an + // error code response that will be more useful to the user than an error + // stating that the server has unexpected disconnected. Save this exception + // in case the server didn't give us a response with an error code. + rex = ex; + } - return SendCommand (command, cancellationToken); + return ParseCommandQueueResponses (message, sender, recipients, responses, rex); } - void Ehlo (CancellationToken cancellationToken) + SmtpResponse SendCommandInternal (string command, CancellationToken cancellationToken) { - SmtpResponse response; - - response = SendEhlo (true, cancellationToken); - - // Some SMTP servers do not accept an EHLO after authentication (despite the rfc saying it is required). - if (authenticated && response.StatusCode == SmtpStatusCode.BadCommandSequence) - return; - - if (response.StatusCode != SmtpStatusCode.Ok) { - // Try sending HELO instead... - response = SendEhlo (false, cancellationToken); - if (response.StatusCode != SmtpStatusCode.Ok) - throw new SmtpCommandException (SmtpErrorCode.UnexpectedStatusCode, response.StatusCode, response.Response); - } else { - // Clear the extensions - capabilities = SmtpCapabilities.None; - AuthenticationMechanisms.Clear (); - MaxSize = 0; - - var lines = response.Response.Split ('\n'); - for (int i = 0; i < lines.Length; i++) { - // Outlook.com replies with "250-8bitmime" instead of "250-8BITMIME" - // (strangely, it correctly capitalizes all other extensions...) - var capability = lines[i].Trim ().ToUpperInvariant (); - - if (capability.StartsWith ("AUTH", StringComparison.Ordinal)) { - int index = 4; - - capabilities |= SmtpCapabilities.Authentication; - - if (index < capability.Length && capability[index] == '=') - index++; - - var mechanisms = capability.Substring (index); - foreach (var mechanism in mechanisms.Split (new [] { ' ' }, StringSplitOptions.RemoveEmptyEntries)) - AuthenticationMechanisms.Add (mechanism); - } else if (capability.StartsWith ("SIZE", StringComparison.Ordinal)) { - int index = 4; - uint size; - - capabilities |= SmtpCapabilities.Size; - - while (index < capability.Length && char.IsWhiteSpace (capability[index])) - index++; - - if (uint.TryParse (capability.Substring (index), out size)) - MaxSize = size; - } else if (capability == "DSN") { - capabilities |= SmtpCapabilities.Dsn; - } else if (capability == "BINARYMIME") { - capabilities |= SmtpCapabilities.BinaryMime; - } else if (capability == "CHUNKING") { - capabilities |= SmtpCapabilities.Chunking; - } else if (capability == "ENHANCEDSTATUSCODES") { - capabilities |= SmtpCapabilities.EnhancedStatusCodes; - } else if (capability == "8BITMIME") { - capabilities |= SmtpCapabilities.EightBitMime; - } else if (capability == "PIPELINING") { - capabilities |= SmtpCapabilities.Pipelining; - } else if (capability == "STARTTLS") { - capabilities |= SmtpCapabilities.StartTLS; - } else if (capability == "SMTPUTF8") { - capabilities |= SmtpCapabilities.UTF8; - } - } + try { + return Stream!.SendCommand (command, cancellationToken); + } catch { + Disconnect (uri!.Host, uri.Port, GetSecureSocketOptions (uri), false); + throw; } } /// - /// Authenticates using the supplied credentials. + /// Send a custom command to the SMTP server. /// /// - /// If the SMTP server supports authentication, then the SASL mechanisms - /// that both the client and server support are tried in order of greatest - /// security to weakest security. Once a SASL authentication mechanism is - /// found that both client and server support, the credentials are used to - /// authenticate. - /// If, on the other hand, authentication is not supported by the SMTP - /// server, then this method will throw . - /// The property can be checked for the - /// flag to make sure the - /// SMTP server supports authentication before calling this method. - /// To prevent the usage of certain authentication mechanisms, - /// simply remove them from the hash set - /// before calling this method. + /// Sends a custom command to the SMTP server. + /// The command string should not include the terminating \r\n sequence. /// - /// The text encoding to use for the user's credentials. - /// The user's credentials. + /// The command response. + /// The command. /// The cancellation token. /// - /// is null. - /// -or- - /// is null. + /// is . + /// + /// + /// The has been disposed. /// /// /// The is not connected. /// - /// - /// The is already authenticated. - /// - /// - /// The SMTP server does not support authentication. - /// /// - /// The operation was canceled via the cancellation token. - /// - /// - /// Authentication using the supplied credentials has failed. - /// - /// - /// A SASL authentication error occurred. + /// The operation has been canceled. /// /// /// An I/O error occurred. /// - /// - /// The SMTP command failed. - /// /// - /// An SMTP protocol error occurred. + /// An SMTP protocol exception occurred. /// - public override void Authenticate (Encoding encoding, ICredentials credentials, CancellationToken cancellationToken = default (CancellationToken)) + protected SmtpResponse SendCommand (string command, CancellationToken cancellationToken = default) { - if (encoding == null) - throw new ArgumentNullException (nameof (encoding)); + if (command == null) + throw new ArgumentNullException (nameof (command)); - if (credentials == null) - throw new ArgumentNullException (nameof (credentials)); + CheckDisposed (); + + if (!IsConnected) + throw new ServiceNotConnectedException ("The SmtpClient must be connected before you can send commands."); + + if (!command.EndsWith ("\r\n", StringComparison.Ordinal)) + command += "\r\n"; + + return SendCommandInternal (command, cancellationToken); + } + + static bool ReadNextLine (string text, ref int index, out int lineStartIndex, out int lineEndIndex) + { + lineStartIndex = 0; + lineEndIndex = 0; + + if (index >= text.Length) + return false; + + lineStartIndex = index; + lineEndIndex = index; + + do { + char c = text[index++]; + + if (c == '\n') + break; + + // Only update lineEndIndex when we see a non-whitespace character. This effectively Trim()'s the end. + if (!char.IsWhiteSpace (c)) + lineEndIndex = index; + } while (index < text.Length); + + return true; + } + + static bool IsCapability (string capability, string text, int startIndex, int endIndex, bool hasValue = false) + { + int length = endIndex - startIndex; + + if (hasValue) { + if (length <= capability.Length) + return false; + } else { + if (length != capability.Length) + return false; + } + + if (string.Compare (text, startIndex, capability, 0, capability.Length, StringComparison.OrdinalIgnoreCase) != 0) + return false; + + if (hasValue) { + int index = startIndex + capability.Length; + + return length > capability.Length && (text[index] == ' ' || text[index] == '='); + } + + return true; + } + + void AddAuthenticationMechanisms (string mechanisms, int startIndex, int endIndex) + { + int index = startIndex; + + do { + while (index < endIndex && char.IsWhiteSpace (mechanisms[index])) + index++; + + int mechanismIndex = index; + + while (index < endIndex && !char.IsWhiteSpace (mechanisms[index])) + index++; + + if (index > mechanismIndex) { + var mechanism = mechanisms.Substring (mechanismIndex, index - mechanismIndex); + + AuthenticationMechanisms.Add (mechanism); + } + } while (index < endIndex); + } + + void SetMaxSize (string capability, int startIndex, int endIndex) + { + int index = startIndex; + + while (index < endIndex && char.IsWhiteSpace (capability[index])) + index++; + +#if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER + var value = capability.AsSpan (index, endIndex - index); +#else + var value = capability.Substring (index, endIndex - index); +#endif + + if (index < endIndex && uint.TryParse (value, NumberStyles.None, CultureInfo.InvariantCulture, out uint size)) + MaxSize = size; + } + + void UpdateCapabilities (SmtpResponse response) + { + // Clear the extensions except STARTTLS so that this capability stays set after a STARTTLS command. + capabilities &= SmtpCapabilities.StartTLS; + AuthenticationMechanisms.Clear (); + MaxSize = 0; + + string text = response.Response; + int index = 0; + + while (ReadNextLine (text, ref index, out int lineStartIndex, out int lineEndIndex)) { + if (IsCapability ("AUTH", text, lineStartIndex, lineEndIndex, true)) { + int startIndex = lineStartIndex + 5; + + AddAuthenticationMechanisms (text, startIndex, lineEndIndex); + capabilities |= SmtpCapabilities.Authentication; + } else if (IsCapability ("X-EXPS", text, lineStartIndex, lineEndIndex, true)) { + int startIndex = lineStartIndex + 7; + + AddAuthenticationMechanisms (text, startIndex, lineEndIndex); + capabilities |= SmtpCapabilities.Authentication; + } else if (IsCapability ("SIZE", text, lineStartIndex, lineEndIndex, true)) { + int startIndex = lineStartIndex + 5; + + SetMaxSize (text, startIndex, lineEndIndex); + capabilities |= SmtpCapabilities.Size; + } else if (IsCapability ("DSN", text, lineStartIndex, lineEndIndex)) { + capabilities |= SmtpCapabilities.Dsn; + } else if (IsCapability ("BINARYMIME", text, lineStartIndex, lineEndIndex)) { + capabilities |= SmtpCapabilities.BinaryMime; + } else if (IsCapability ("CHUNKING", text, lineStartIndex, lineEndIndex)) { + capabilities |= SmtpCapabilities.Chunking; + } else if (IsCapability ("ENHANCEDSTATUSCODES", text, lineStartIndex, lineEndIndex)) { + capabilities |= SmtpCapabilities.EnhancedStatusCodes; + } else if (IsCapability ("8BITMIME", text, lineStartIndex, lineEndIndex)) { + capabilities |= SmtpCapabilities.EightBitMime; + } else if (IsCapability ("PIPELINING", text, lineStartIndex, lineEndIndex)) { + capabilities |= SmtpCapabilities.Pipelining; + } else if (IsCapability ("STARTTLS", text, lineStartIndex, lineEndIndex)) { + capabilities |= SmtpCapabilities.StartTLS; + } else if (IsCapability ("SMTPUTF8", text, lineStartIndex, lineEndIndex)) { + capabilities |= SmtpCapabilities.UTF8; + } else if (IsCapability ("REQUIRETLS", text, lineStartIndex, lineEndIndex)) { + capabilities |= SmtpCapabilities.RequireTLS; + } + } + } + + string CreateEhloCommand (string helo) + { + string domain; + + if (!string.IsNullOrEmpty (LocalDomain)) { + if (IPAddress.TryParse (LocalDomain, out var ip)) { + if (ip.IsIPv4MappedToIPv6) { + try { + ip = ip.MapToIPv4 (); + } catch (ArgumentOutOfRangeException) { + // .NET 4.5.2 bug on Windows 7 SP1 (issue #814) + } + } + + if (ip.AddressFamily == AddressFamily.InterNetworkV6) + return string.Format ("{0} [IPv6:{1}]\r\n", helo, ip); + + return string.Format ("{0} [{1}]\r\n", helo, ip); + } else { + domain = LocalDomain!; + } + } else { + domain = DefaultLocalDomain; + } + + return string.Format ("{0} {1}\r\n", helo, domain); + } + + SmtpResponse SendEhlo (bool connecting, string helo, CancellationToken cancellationToken) + { + var command = CreateEhloCommand (helo); + + if (connecting) + return Stream!.SendCommand (command, cancellationToken); + + return SendCommandInternal (command, cancellationToken); + } + + void Ehlo (bool connecting, CancellationToken cancellationToken) + { + var response = SendEhlo (connecting, "EHLO", cancellationToken); + + if (response.StatusCode != SmtpStatusCode.Ok) { + // Try sending HELO instead... + response = SendEhlo (connecting, "HELO", cancellationToken); + + if (response.StatusCode != SmtpStatusCode.Ok) + throw new SmtpCommandException (SmtpErrorCode.UnexpectedStatusCode, response.StatusCode, response.Response); + } else { + UpdateCapabilities (response); + } + } + + void ValidateArguments (SaslMechanism mechanism) + { + if (mechanism == null) + throw new ArgumentNullException (nameof (mechanism)); CheckDisposed (); @@ -599,116 +1004,284 @@ void Ehlo (CancellationToken cancellationToken) if ((capabilities & SmtpCapabilities.Authentication) == 0) throw new NotSupportedException ("The SMTP server does not support authentication."); - var uri = new Uri ("smtp://" + host); - AuthenticationException authException = null; - SmtpResponse response; - SaslMechanism sasl; - bool tried = false; - string challenge; - string command; + mechanism.ChannelBindingContext = Stream.Stream as IChannelBindingContext; + mechanism.Uri = new Uri ($"smtp://{uri.Host}"); + } - foreach (var authmech in SaslMechanism.AuthMechanismRank) { - if (!AuthenticationMechanisms.Contains (authmech)) - continue; + /// + /// Authenticate using the specified SASL mechanism. + /// + /// + /// Authenticates using the specified SASL mechanism. + /// For a list of available SASL authentication mechanisms supported by the server, + /// check the property after the service has been + /// connected. + /// + /// The SASL mechanism. + /// The cancellation token. + /// + /// is . + /// + /// + /// The is not connected. + /// + /// + /// The is already authenticated. + /// + /// + /// The SMTP server does not support authentication. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Authentication using the supplied credentials has failed. + /// + /// + /// A SASL authentication error occurred. + /// + /// + /// An I/O error occurred. + /// + /// + /// The SMTP command failed. + /// + /// + /// An SMTP protocol error occurred. + /// + public override void Authenticate (SaslMechanism mechanism, CancellationToken cancellationToken = default) + { + ValidateArguments (mechanism); - if ((sasl = SaslMechanism.Create (authmech, uri, encoding, credentials)) == null) - continue; + cancellationToken.ThrowIfCancellationRequested (); - tried = true; + using var operation = StartNetworkOperation (NetworkOperationKind.Authenticate); - cancellationToken.ThrowIfCancellationRequested (); + try { + SaslException? saslException = null; + SmtpResponse response; + string challenge; + string command; // send an initial challenge if the mechanism supports it - if (sasl.SupportsInitialResponse) { - challenge = sasl.Challenge (null); - command = string.Format ("AUTH {0} {1}", authmech, challenge); + if (mechanism.SupportsInitialResponse) { + challenge = mechanism.Challenge (null, cancellationToken); + command = string.Format ("AUTH {0} {1}\r\n", mechanism.MechanismName, challenge); } else { - command = string.Format ("AUTH {0}", authmech); + command = string.Format ("AUTH {0}\r\n", mechanism.MechanismName); } - response = SendCommand (command, cancellationToken); - - if (response.StatusCode == SmtpStatusCode.AuthenticationMechanismTooWeak) - continue; - - SaslException saslException = null; + detector.IsAuthenticating = true; try { - while (!sasl.IsAuthenticated) { - if (response.StatusCode != SmtpStatusCode.AuthenticationChallenge) - throw new SmtpCommandException (SmtpErrorCode.UnexpectedStatusCode, response.StatusCode, response.Response); - - challenge = sasl.Challenge (response.Response); - response = SendCommand (challenge, cancellationToken); + response = SendCommandInternal (command, cancellationToken); + + if (response.StatusCode == SmtpStatusCode.AuthenticationMechanismTooWeak) + throw new AuthenticationException (response.Response); + + try { + while (response.StatusCode == SmtpStatusCode.AuthenticationChallenge) { + challenge = mechanism.Challenge (response.Response, cancellationToken); + response = SendCommandInternal (challenge + "\r\n", cancellationToken); + } + + saslException = null; + } catch (SaslException ex) { + // reset the authentication state + response = SendCommandInternal ("\r\n", cancellationToken); + saslException = ex; } - - saslException = null; - } catch (SaslException ex) { - // reset the authentication state - response = SendCommand (string.Empty, cancellationToken); - saslException = ex; + } finally { + detector.IsAuthenticating = false; } if (response.StatusCode == SmtpStatusCode.AuthenticationSuccessful) { - if (sasl.NegotiatedSecurityLayer) - Ehlo (cancellationToken); + if (mechanism.NegotiatedSecurityLayer) + Ehlo (false, cancellationToken); authenticated = true; OnAuthenticated (response.Response); return; } - var message = string.Format ("{0}: {1}", response.StatusCode, response.Response); + var message = string.Format (CultureInfo.InvariantCulture, "{0}: {1}", (int) response.StatusCode, response.Response); if (saslException != null) - authException = new AuthenticationException (message, saslException); - else - authException = new AuthenticationException (message); - } + throw new AuthenticationException (message, saslException); - if (tried) - throw authException ?? new AuthenticationException (); - - throw new NotSupportedException ("No compatible authentication mechanisms found."); + throw new AuthenticationException (message); + } catch (Exception ex) { + operation.SetError (ex); + throw; + } } - internal void ReplayConnect (string hostName, Stream replayStream, CancellationToken cancellationToken = default (CancellationToken)) + [MemberNotNull (nameof (Stream), nameof (uri))] + void ValidateArguments (Encoding encoding, ICredentials credentials) { + if (encoding == null) + throw new ArgumentNullException (nameof (encoding)); + + if (credentials == null) + throw new ArgumentNullException (nameof (credentials)); + CheckDisposed (); - if (hostName == null) - throw new ArgumentNullException (nameof (hostName)); + if (!IsConnected) + throw new ServiceNotConnectedException ("The SmtpClient must be connected before you can authenticate."); - if (replayStream == null) - throw new ArgumentNullException (nameof (replayStream)); + if (IsAuthenticated) + throw new InvalidOperationException ("The SmtpClient is already authenticated."); - Stream = new SmtpStream (replayStream, null, ProtocolLogger); - capabilities = SmtpCapabilities.None; - AuthenticationMechanisms.Clear (); - host = hostName; - secure = false; - MaxSize = 0; + if ((capabilities & SmtpCapabilities.Authentication) == 0) + throw new NotSupportedException ("The SMTP server does not support authentication."); + } + + /// + /// Authenticate using the supplied credentials. + /// + /// + /// Authenticates using the supplied credentials. + /// If the SMTP server supports authentication, then the SASL mechanisms + /// that both the client and server support (not including any OAUTH mechanisms) + /// are tried in order of greatest security to weakest security. Once a SASL + /// authentication mechanism is found that both client and server support, the + /// credentials are used to authenticate. + /// If, on the other hand, authentication is not supported by the SMTP + /// server, then this method will throw . + /// The property can be checked for the + /// flag to make sure the + /// SMTP server supports authentication before calling this method. + /// To prevent the usage of certain authentication mechanisms, + /// simply remove them from the hash set + /// before calling this method. + /// + /// The text encoding to use for the user's credentials. + /// The user's credentials. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// The is not connected. + /// + /// + /// The is already authenticated. + /// + /// + /// The SMTP server does not support authentication. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Authentication using the supplied credentials has failed. + /// + /// + /// A SASL authentication error occurred. + /// + /// + /// An I/O error occurred. + /// + /// + /// The SMTP command failed. + /// + /// + /// An SMTP protocol error occurred. + /// + public override void Authenticate (Encoding encoding, ICredentials credentials, CancellationToken cancellationToken = default) + { + ValidateArguments (encoding, credentials); + + using var operation = StartNetworkOperation (NetworkOperationKind.Authenticate); try { - // read the greeting - var response = Stream.ReadResponse (cancellationToken); + var saslUri = new Uri ($"smtp://{uri.Host}"); + AuthenticationException? authException = null; + SaslException? saslException; + SmtpResponse response; + SaslMechanism? sasl; + bool tried = false; + string challenge; + string command; - if (response.StatusCode != SmtpStatusCode.ServiceReady) - throw new SmtpCommandException (SmtpErrorCode.UnexpectedStatusCode, response.StatusCode, response.Response); + foreach (var authmech in SaslMechanism.Rank (AuthenticationMechanisms)) { + var cred = credentials.GetCredential (uri, authmech); - // Send EHLO and get a list of supported extensions - Ehlo (cancellationToken); + if (cred == null || (sasl = SaslMechanism.Create (authmech, encoding, cred)) == null) + continue; - connected = true; - } catch { - Stream.Dispose (); - Stream = null; + sasl.ChannelBindingContext = Stream.Stream as IChannelBindingContext; + sasl.Uri = saslUri; + + tried = true; + + cancellationToken.ThrowIfCancellationRequested (); + + // send an initial challenge if the mechanism supports it + if (sasl.SupportsInitialResponse) { + challenge = sasl.Challenge (null, cancellationToken); + command = string.Format ("AUTH {0} {1}\r\n", authmech, challenge); + } else { + command = string.Format ("AUTH {0}\r\n", authmech); + } + + detector.IsAuthenticating = true; + saslException = null; + + try { + response = SendCommandInternal (command, cancellationToken); + + if (response.StatusCode == SmtpStatusCode.AuthenticationMechanismTooWeak) + continue; + + try { + while (response.StatusCode == SmtpStatusCode.AuthenticationChallenge) { + challenge = sasl.Challenge (response.Response, cancellationToken); + response = SendCommandInternal (challenge + "\r\n", cancellationToken); + } + + saslException = null; + } catch (SaslException ex) { + // reset the authentication state + response = SendCommandInternal ("\r\n", cancellationToken); + saslException = ex; + } + } finally { + detector.IsAuthenticating = false; + } + + if (response.StatusCode == SmtpStatusCode.AuthenticationSuccessful) { + if (sasl.NegotiatedSecurityLayer) + Ehlo (false, cancellationToken); + authenticated = true; + OnAuthenticated (response.Response); + return; + } + + var message = string.Format (CultureInfo.InvariantCulture, "{0}: {1}", (int) response.StatusCode, response.Response); + Exception inner; + + if (saslException != null) + inner = new SmtpCommandException (SmtpErrorCode.UnexpectedStatusCode, response.StatusCode, response.Response, saslException); + else + inner = new SmtpCommandException (SmtpErrorCode.UnexpectedStatusCode, response.StatusCode, response.Response); + + authException = new AuthenticationException (message, inner); + } + + if (tried) + throw authException ?? new AuthenticationException (); + + throw new NotSupportedException ("No compatible authentication mechanisms found."); + } catch (Exception ex) { + operation.SetError (ex); throw; } - - OnConnected (); } - static void ComputeDefaultValues (string host, ref int port, ref SecureSocketOptions options, out Uri uri, out bool starttls) + internal static void ComputeDefaultValues (string host, ref int port, ref SecureSocketOptions options, out Uri uri, out bool starttls) { switch (options) { default: @@ -728,28 +1301,124 @@ static void ComputeDefaultValues (string host, ref int port, ref SecureSocketOpt break; } + if (IPAddress.TryParse (host, out var ip) && ip.AddressFamily == AddressFamily.InterNetworkV6) + host = "[" + host + "]"; + switch (options) { case SecureSocketOptions.StartTlsWhenAvailable: - uri = new Uri ("smtp://" + host + ":" + port + "/?starttls=when-available"); + uri = new Uri (string.Format (CultureInfo.InvariantCulture, "smtp://{0}:{1}/?starttls=when-available", host, port)); starttls = true; break; case SecureSocketOptions.StartTls: - uri = new Uri ("smtp://" + host + ":" + port + "/?starttls=always"); + uri = new Uri (string.Format (CultureInfo.InvariantCulture, "smtp://{0}:{1}/?starttls=always", host, port)); starttls = true; break; case SecureSocketOptions.SslOnConnect: - uri = new Uri ("smtps://" + host + ":" + port); + uri = new Uri (string.Format (CultureInfo.InvariantCulture, "smtps://{0}:{1}", host, port)); starttls = false; break; default: - uri = new Uri ("smtp://" + host + ":" + port); + uri = new Uri (string.Format (CultureInfo.InvariantCulture, "smtp://{0}:{1}", host, port)); starttls = false; break; } } + void SslHandshake (SslStream ssl, string host, CancellationToken cancellationToken) + { +#if NET5_0_OR_GREATER + ssl.AuthenticateAsClient (GetSslClientAuthenticationOptions (host, ValidateRemoteCertificate)); +#else + ssl.AuthenticateAsClient (host, ClientCertificates, SslProtocols, CheckCertificateRevocation); +#endif + } + + void RecordClientDisconnected (Exception? ex) + { +#if NET6_0_OR_GREATER + metrics?.RecordClientDisconnected (clientConnectedTimestamp, uri!, ex); +#endif + clientConnectedTimestamp = 0; + } + + void PostConnect (Stream stream, string host, int port, SecureSocketOptions options, bool starttls, CancellationToken cancellationToken) + { + clientConnectedTimestamp = Stopwatch.GetTimestamp (); + + try { + ProtocolLogger.LogConnect (uri!); + } catch { + stream.Dispose (); + secure = false; + throw; + } + + Stream = new SmtpStream (stream, ProtocolLogger); + + try { + // read the greeting + var response = Stream.ReadResponse (cancellationToken); + + if (response.StatusCode != SmtpStatusCode.ServiceReady) + throw new SmtpCommandException (SmtpErrorCode.UnexpectedStatusCode, response.StatusCode, response.Response); + + // Send EHLO and get a list of supported extensions + Ehlo (true, cancellationToken); + + if (options == SecureSocketOptions.StartTls && (capabilities & SmtpCapabilities.StartTLS) == 0) + throw new NotSupportedException ("The SMTP server does not support the STARTTLS extension."); + + if (starttls && (capabilities & SmtpCapabilities.StartTLS) != 0) { + response = Stream.SendCommand ("STARTTLS\r\n", cancellationToken); + if (response.StatusCode != SmtpStatusCode.ServiceReady) + throw new SmtpCommandException (SmtpErrorCode.UnexpectedStatusCode, response.StatusCode, response.Response); + + try { + var tls = new ExtendedSslStream (stream, false, ValidateRemoteCertificate); + Stream.SetStream (tls); + + SslHandshake (tls, host, cancellationToken); + } catch (Exception ex) { + throw SslHandshakeException.Create (ref sslValidationInfo, ex, true, "SMTP", host, port, 465, 25, 587); + } + + secure = true; + + // Send EHLO again and get the new list of supported extensions + Ehlo (true, cancellationToken); + } + + connected = true; + } catch (Exception ex) { + RecordClientDisconnected (ex); + Stream.Dispose (); + secure = false; + Stream = null; + throw; + } + + OnConnected (host, port, options); + } + + void ValidateArguments (string host, int port) + { + if (host == null) + throw new ArgumentNullException (nameof (host)); + + if (host.Length == 0) + throw new ArgumentException ("The host name cannot be empty.", nameof (host)); + + if (port < 0 || port > 65535) + throw new ArgumentOutOfRangeException (nameof (port)); + + CheckDisposed (); + + if (IsConnected) + throw new InvalidOperationException ("The SmtpClient is already connected."); + } + /// - /// Establishes a connection to the specified SMTP or SMTP/S server. + /// Establish a connection to the specified SMTP or SMTP/S server. /// /// /// Establishes a connection to the specified SMTP or SMTP/S server. @@ -782,7 +1451,7 @@ static void ComputeDefaultValues (string host, ref int port, ref SecureSocketOpt /// The secure socket options to when connecting. /// The cancellation token. /// - /// is null. + /// is . /// /// /// is not between 0 and 65535. @@ -807,6 +1476,9 @@ static void ComputeDefaultValues (string host, ref int port, ref SecureSocketOpt /// /// A socket error occurred trying to connect to the remote host. /// + /// + /// An error occurred during the SSL/TLS negotiations. + /// /// /// An I/O error occurred. /// @@ -816,179 +1488,141 @@ static void ComputeDefaultValues (string host, ref int port, ref SecureSocketOpt /// /// An SMTP protocol error occurred. /// - public override void Connect (string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default (CancellationToken)) + public override void Connect (string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default) { - if (host == null) - throw new ArgumentNullException (nameof (host)); - - if (host.Length == 0) - throw new ArgumentException ("The host name cannot be empty.", nameof (host)); - - if (port < 0 || port > 65535) - throw new ArgumentOutOfRangeException (nameof (port)); - - CheckDisposed (); - - if (IsConnected) - throw new InvalidOperationException ("The SmtpClient is already connected."); + ValidateArguments (host, port); capabilities = SmtpCapabilities.None; AuthenticationMechanisms.Clear (); MaxSize = 0; - SmtpResponse response; - Stream stream; - bool starttls; - Uri uri; - - ComputeDefaultValues (host, ref port, ref options, out uri, out starttls); - -#if !NETFX_CORE -#if NETSTANDARD - var ipAddresses = Dns.GetHostAddressesAsync (uri.DnsSafeHost).GetAwaiter ().GetResult (); -#else - var ipAddresses = Dns.GetHostAddresses (uri.DnsSafeHost); -#endif - Socket socket = null; + ComputeDefaultValues (host, ref port, ref options, out uri, out var starttls); - for (int i = 0; i < ipAddresses.Length; i++) { - socket = new Socket (ipAddresses[i].AddressFamily, SocketType.Stream, ProtocolType.Tcp); - - try { - cancellationToken.ThrowIfCancellationRequested (); - - if (LocalEndPoint != null) - socket.Bind (LocalEndPoint); - - socket.Connect (ipAddresses[i], port); - break; - } catch (OperationCanceledException) { - socket.Dispose (); - socket = null; - throw; - } catch { - socket.Dispose (); - socket = null; - - if (i + 1 == ipAddresses.Length) - throw; - } - } - - if (socket == null) - throw new IOException (string.Format ("Failed to resolve host: {0}", host)); - - this.host = host; - - if (options == SecureSocketOptions.SslOnConnect) { - var ssl = new SslStream (new NetworkStream (socket, true), false, ValidateRemoteCertificate); - - try { -#if NETSTANDARD - ssl.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, true).GetAwaiter ().GetResult (); -#else - ssl.AuthenticateAsClient (host, ClientCertificates, SslProtocols, true); -#endif - } catch { - ssl.Dispose (); - throw; - } - - secure = true; - stream = ssl; - } else { - stream = new NetworkStream (socket, true); - secure = false; - } -#else - var protection = options == SecureSocketOptions.SslOnConnect ? SocketProtectionLevel.Tls12 : SocketProtectionLevel.PlainSocket; - var socket = new StreamSocket (); + using var operation = StartNetworkOperation (NetworkOperationKind.Connect); try { - cancellationToken.ThrowIfCancellationRequested (); - socket.ConnectAsync (new HostName (host), port.ToString (), protection) - .AsTask (cancellationToken) - .GetAwaiter () - .GetResult (); - } catch { - socket.Dispose (); - throw; - } - - stream = new DuplexStream (socket.InputStream.AsStreamForRead (0), socket.OutputStream.AsStreamForWrite (0)); - secure = options == SecureSocketOptions.SslOnConnect; - this.host = host; -#endif - - if (stream.CanTimeout) { + var stream = ConnectNetwork (host, port, cancellationToken); stream.WriteTimeout = timeout; stream.ReadTimeout = timeout; - } - ProtocolLogger.LogConnect (uri); + if (options == SecureSocketOptions.SslOnConnect) { + var ssl = new ExtendedSslStream (stream, false, ValidateRemoteCertificate); - Stream = new SmtpStream (stream, socket, ProtocolLogger); + try { + SslHandshake (ssl, host, cancellationToken); + } catch (Exception ex) { + ssl.Dispose (); - try { - // read the greeting - response = Stream.ReadResponse (cancellationToken); + throw SslHandshakeException.Create (ref sslValidationInfo, ex, false, "SMTP", host, port, 465, 25, 587); + } - if (response.StatusCode != SmtpStatusCode.ServiceReady) - throw new SmtpCommandException (SmtpErrorCode.UnexpectedStatusCode, response.StatusCode, response.Response); + secure = true; + stream = ssl; + } else { + secure = false; + } - // Send EHLO and get a list of supported extensions - Ehlo (cancellationToken); + PostConnect (stream, host, port, options, starttls, cancellationToken); + } catch (Exception ex) { + operation.SetError (ex); + throw; + } + } - if (options == SecureSocketOptions.StartTls && (capabilities & SmtpCapabilities.StartTLS) == 0) - throw new NotSupportedException ("The SMTP server does not support the STARTTLS extension."); + void ValidateArguments (Socket socket, string host, int port) + { + if (socket == null) + throw new ArgumentNullException (nameof (socket)); - if (starttls && (capabilities & SmtpCapabilities.StartTLS) != 0) { - response = SendCommand ("STARTTLS", cancellationToken); - if (response.StatusCode != SmtpStatusCode.ServiceReady) - throw new SmtpCommandException (SmtpErrorCode.UnexpectedStatusCode, response.StatusCode, response.Response); + if (!socket.Connected) + throw new ArgumentException ("The socket is not connected.", nameof (socket)); -#if !NETFX_CORE - var tls = new SslStream (stream, false, ValidateRemoteCertificate); -#if NETSTANDARD - tls.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, true).GetAwaiter ().GetResult (); -#else - tls.AuthenticateAsClient (host, ClientCertificates, SslProtocols, true); -#endif - Stream.Stream = tls; -#else - socket.UpgradeToSslAsync (SocketProtectionLevel.Tls12, new HostName (host)) - .AsTask (cancellationToken) - .GetAwaiter () - .GetResult (); -#endif + ValidateArguments (host, port); + } - secure = true; + /// + /// Establish a connection to the specified SMTP or SMTP/S server using the provided socket. + /// + /// + /// Establishes a connection to the specified SMTP or SMTP/S server using the provided socket. + /// If the has a value of + /// , then the is used + /// to determine the default security options. If the has a value + /// of 465, then the default options used will be + /// . All other values will use + /// . + /// Once a connection is established, properties such as + /// and will be + /// populated. + /// With the exception of using the to determine the + /// default to use when the value + /// is , the and + /// parameters are only used for logging purposes. + /// + /// The socket to use for the connection. + /// The host name to connect to. + /// The port to connect to. If the specified port is 0, then the default port will be used. + /// The secure socket options to when connecting. + /// The cancellation token. + /// + /// is . + /// -or- + /// is . + /// + /// + /// is not between 0 and 65535. + /// + /// + /// is not connected. + /// -or- + /// The is a zero-length string. + /// + /// + /// The has been disposed. + /// + /// + /// The is already connected. + /// + /// + /// was set to + /// + /// and the SMTP server does not support the STARTTLS extension. + /// + /// + /// The operation was canceled. + /// + /// + /// An error occurred during the SSL/TLS negotiations. + /// + /// + /// An I/O error occurred. + /// + /// + /// An SMTP command failed. + /// + /// + /// An SMTP protocol error occurred. + /// + public override void Connect (Socket socket, string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default) + { + ValidateArguments (socket, host, port); - // Send EHLO again and get the new list of supported extensions - Ehlo (cancellationToken); - } + Connect (new NetworkStream (socket, true), host, port, options, cancellationToken); + } - connected = true; - } catch { - Stream.Dispose (); - secure = false; - Stream = null; - throw; - } + void ValidateArguments (Stream stream, string host, int port) + { + if (stream == null) + throw new ArgumentNullException (nameof (stream)); - OnConnected (); + ValidateArguments (host, port); } -#if !NETFX_CORE /// - /// Establish a connection to the specified SMTP or SMTP/S server using the provided socket. + /// Establish a connection to the specified SMTP or SMTP/S server using the provided stream. /// /// - /// Establishes a connection to the specified SMTP or SMTP/S server. - /// If the has a value of 0, then the - /// parameter is used to determine the default port to - /// connect to. The default port used with - /// is 465. All other values will use a default port of 25. + /// Establishes a connection to the specified SMTP or SMTP/S server using the provided stream. /// If the has a value of /// , then the is used /// to determine the default security options. If the has a value @@ -998,30 +1632,25 @@ static void ComputeDefaultValues (string host, ref int port, ref SecureSocketOpt /// Once a connection is established, properties such as /// and will be /// populated. - /// The connection established by any of the - /// Connect - /// methods may be re-used if an application wishes to send multiple messages - /// to the same SMTP server. Since connecting and authenticating can be expensive - /// operations, re-using a connection can significantly improve performance when - /// sending a large number of messages to the same SMTP server over a short - /// period of time./ + /// With the exception of using the to determine the + /// default to use when the value + /// is , the and + /// parameters are only used for logging purposes. /// - /// The socket to use for the connection. + /// The stream to use for the connection. /// The host name to connect to. /// The port to connect to. If the specified port is 0, then the default port will be used. /// The secure socket options to when connecting. /// The cancellation token. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// is not between 0 and 65535. /// /// - /// is not connected. - /// -or- /// The is a zero-length string. /// /// @@ -1038,6 +1667,9 @@ static void ComputeDefaultValues (string host, ref int port, ref SecureSocketOpt /// /// The operation was canceled. /// + /// + /// An error occurred during the SSL/TLS negotiations. + /// /// /// An I/O error occurred. /// @@ -1047,130 +1679,66 @@ static void ComputeDefaultValues (string host, ref int port, ref SecureSocketOpt /// /// An SMTP protocol error occurred. /// - public void Connect (Socket socket, string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default (CancellationToken)) + public override void Connect (Stream stream, string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default) { - if (socket == null) - throw new ArgumentNullException (nameof (socket)); - - if (!socket.Connected) - throw new ArgumentException ("The socket is not connected.", nameof (socket)); - - if (host == null) - throw new ArgumentNullException (nameof (host)); - - if (host.Length == 0) - throw new ArgumentException ("The host name cannot be empty.", nameof (host)); - - if (port < 0 || port > 65535) - throw new ArgumentOutOfRangeException (nameof (port)); - - CheckDisposed (); - - if (IsConnected) - throw new InvalidOperationException ("The SmtpClient is already connected."); + ValidateArguments (stream, host, port); capabilities = SmtpCapabilities.None; AuthenticationMechanisms.Clear (); MaxSize = 0; - SmtpResponse response; - Stream stream; - bool starttls; - Uri uri; - - ComputeDefaultValues (host, ref port, ref options, out uri, out starttls); - - this.host = host; - - if (options == SecureSocketOptions.SslOnConnect) { - var ssl = new SslStream (new NetworkStream (socket, true), false, ValidateRemoteCertificate); - - try { -#if NETSTANDARD - ssl.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, true).GetAwaiter ().GetResult (); -#else - ssl.AuthenticateAsClient (host, ClientCertificates, SslProtocols, true); -#endif - } catch { - ssl.Dispose (); - throw; - } - - secure = true; - stream = ssl; - } else { - stream = new NetworkStream (socket, true); - secure = false; - } - - if (stream.CanTimeout) { - stream.WriteTimeout = timeout; - stream.ReadTimeout = timeout; - } - - ProtocolLogger.LogConnect (uri); + ComputeDefaultValues (host, ref port, ref options, out uri, out var starttls); - Stream = new SmtpStream (stream, socket, ProtocolLogger); + using var operation = StartNetworkOperation (NetworkOperationKind.Connect); try { - // read the greeting - response = Stream.ReadResponse (cancellationToken); - - if (response.StatusCode != SmtpStatusCode.ServiceReady) - throw new SmtpCommandException (SmtpErrorCode.UnexpectedStatusCode, response.StatusCode, response.Response); + Stream network; - // Send EHLO and get a list of supported extensions - Ehlo (cancellationToken); - - if (options == SecureSocketOptions.StartTls && (capabilities & SmtpCapabilities.StartTLS) == 0) - throw new NotSupportedException ("The SMTP server does not support the STARTTLS extension."); + if (options == SecureSocketOptions.SslOnConnect) { + var ssl = new ExtendedSslStream (stream, false, ValidateRemoteCertificate); - if (starttls && (capabilities & SmtpCapabilities.StartTLS) != 0) { - response = SendCommand ("STARTTLS", cancellationToken); - if (response.StatusCode != SmtpStatusCode.ServiceReady) - throw new SmtpCommandException (SmtpErrorCode.UnexpectedStatusCode, response.StatusCode, response.Response); + try { + SslHandshake (ssl, host, cancellationToken); + } catch (Exception ex) { + ssl.Dispose (); - var tls = new SslStream (stream, false, ValidateRemoteCertificate); -#if NETSTANDARD - tls.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, true).GetAwaiter ().GetResult (); -#else - tls.AuthenticateAsClient (host, ClientCertificates, SslProtocols, true); -#endif - Stream.Stream = tls; + throw SslHandshakeException.Create (ref sslValidationInfo, ex, false, "SMTP", host, port, 465, 25, 587); + } + network = ssl; secure = true; + } else { + network = stream; + secure = false; + } - // Send EHLO again and get the new list of supported extensions - Ehlo (cancellationToken); + if (network.CanTimeout) { + network.WriteTimeout = timeout; + network.ReadTimeout = timeout; } - connected = true; - } catch { - Stream.Dispose (); - secure = false; - Stream = null; + PostConnect (network, host, port, options, starttls, cancellationToken); + } catch (Exception ex) { + operation.SetError (ex); throw; } - - OnConnected (); } -#endif /// /// Disconnect the service. /// /// - /// If is true, a QUIT command will be issued in order to disconnect cleanly. + /// If is , a QUIT command will be issued in order to disconnect cleanly. /// /// /// /// - /// If set to true, a QUIT command will be issued in order to disconnect cleanly. + /// If set to , a QUIT command will be issued in order to disconnect cleanly. /// The cancellation token. /// /// The has been disposed. /// - public override void Disconnect (bool quit, CancellationToken cancellationToken = default (CancellationToken)) + public override void Disconnect (bool quit, CancellationToken cancellationToken = default) { CheckDisposed (); @@ -1179,7 +1747,7 @@ static void ComputeDefaultValues (string host, ref int port, ref SecureSocketOpt if (quit) { try { - SendCommand ("QUIT", cancellationToken); + Stream.SendCommand ("QUIT\r\n", cancellationToken); } catch (OperationCanceledException) { } catch (SmtpProtocolException) { } catch (SmtpCommandException) { @@ -1187,7 +1755,7 @@ static void ComputeDefaultValues (string host, ref int port, ref SecureSocketOpt } } - Disconnect (); + Disconnect (uri.Host, uri.Port, GetSecureSocketOptions (uri), true); } /// @@ -1213,40 +1781,46 @@ static void ComputeDefaultValues (string host, ref int port, ref SecureSocketOpt /// /// An SMTP protocol error occurred. /// - public override void NoOp (CancellationToken cancellationToken = default (CancellationToken)) + public override void NoOp (CancellationToken cancellationToken = default) { CheckDisposed (); if (!IsConnected) throw new ServiceNotConnectedException ("The SmtpClient is not connected."); - var response = SendCommand ("NOOP", cancellationToken); + var response = SendCommandInternal ("NOOP\r\n", cancellationToken); if (response.StatusCode != SmtpStatusCode.Ok) throw new SmtpCommandException (SmtpErrorCode.UnexpectedStatusCode, response.StatusCode, response.Response); } - void Disconnect () + void Disconnect (string? host, int port, SecureSocketOptions options, bool requested) { + // Note: if the uri is null, then the user manually disconnected already. + if (uri != null) + RecordClientDisconnected (null); + capabilities = SmtpCapabilities.None; authenticated = false; connected = false; secure = false; - host = null; + queued.Clear (); + uri = null; if (Stream != null) { Stream.Dispose (); Stream = null; } - OnDisconnected (); + if (host != null) + OnDisconnected (host, port, options, requested); } - #endregion +#endregion #region IMailTransport implementation - static MailboxAddress GetMessageSender (MimeMessage message) + static MailboxAddress? GetMessageSender (MimeMessage message) { if (message.ResentSender != null) return message.ResentSender; @@ -1260,7 +1834,7 @@ static MailboxAddress GetMessageSender (MimeMessage message) return message.From.Mailboxes.FirstOrDefault (); } - static void AddUnique (IList recipients, HashSet unique, IEnumerable mailboxes) + static void AddUnique (List recipients, HashSet unique, IEnumerable mailboxes) { foreach (var mailbox in mailboxes) { if (unique.Add (mailbox.Address)) @@ -1286,55 +1860,6 @@ static IList GetMessageRecipients (MimeMessage message) return recipients; } - [Flags] - enum SmtpExtension { - None = 0, - EightBitMime = 1 << 0, - BinaryMime = 1 << 1, - UTF8 = 1 << 2, - } - - class ContentTransferEncodingVisitor : MimeVisitor - { - readonly SmtpCapabilities Capabilities; - - public ContentTransferEncodingVisitor (SmtpCapabilities capabilities) - { - Capabilities = capabilities; - } - - public SmtpExtension SmtpExtensions { - get; private set; - } - - protected override void VisitMultipart (Multipart multipart) - { - if (multipart.ContentType.IsMimeType ("multipart", "signed")) { - // do not modify children of a multipart/signed - return; - } - - base.VisitMultipart (multipart); - } - - protected override void VisitMimePart (MimePart entity) - { - switch (entity.ContentTransferEncoding) { - case ContentEncoding.EightBit: - // if the server supports the 8BITMIME extension, use it... - if ((Capabilities & SmtpCapabilities.EightBitMime) != 0) { - SmtpExtensions |= SmtpExtension.EightBitMime; - } else { - SmtpExtensions |= SmtpExtension.BinaryMime; - } - break; - case ContentEncoding.Binary: - SmtpExtensions |= SmtpExtension.BinaryMime; - break; - } - } - } - /// /// Invoked when the sender is accepted by the SMTP server. /// @@ -1349,7 +1874,7 @@ protected virtual void OnSenderAccepted (MimeMessage message, MailboxAddress mai } /// - /// Invoked when a recipient is not accepted by the SMTP server. + /// Invoked when the sender is not accepted by the SMTP server. /// /// /// The default implementation throws an appropriate . @@ -1362,23 +1887,6 @@ protected virtual void OnSenderNotAccepted (MimeMessage message, MailboxAddress throw new SmtpCommandException (SmtpErrorCode.SenderNotAccepted, response.StatusCode, mailbox, response.Response); } - void ProcessMailFromResponse (MimeMessage message, MailboxAddress mailbox, SmtpResponse response) - { - switch (response.StatusCode) { - case SmtpStatusCode.Ok: - OnSenderAccepted (message, mailbox, response); - break; - case SmtpStatusCode.MailboxNameNotAllowed: - case SmtpStatusCode.MailboxUnavailable: - OnSenderNotAccepted (message, mailbox, response); - break; - case SmtpStatusCode.AuthenticationRequired: - throw new ServiceNotAuthenticatedException (response.Response); - default: - throw new SmtpCommandException (SmtpErrorCode.UnexpectedStatusCode, response.StatusCode, response.Response); - } - } - /// /// Get the envelope identifier to be used with delivery status notifications. /// @@ -1395,48 +1903,153 @@ void ProcessMailFromResponse (MimeMessage message, MailboxAddress mailbox, SmtpR /// /// The envelope identifier. /// The message. - protected virtual string GetEnvelopeId (MimeMessage message) + protected virtual string? GetEnvelopeId (MimeMessage message) { return null; } - static string GetAddrspec (FormatOptions options, MailboxAddress mailbox) + /// + /// Get or set how much of the message to include in any failed delivery status notifications. + /// + /// + /// Gets or sets how much of the message to include in any failed delivery status notifications. + /// + /// + /// + /// + /// A value indicating how much of the message to include in a failure delivery status notification. + public DeliveryStatusNotificationType DeliveryStatusNotificationType { + get; set; + } + + static void AppendHexEncoded (StringBuilder builder, string value) { - if (options.International) - return MailboxAddress.DecodeAddrspec (mailbox.Address); + int index = 0; + + while (index < value.Length) { + char c = value[index]; + + if (c < 33 || c > 126 || c == (byte) '+' || c == (byte) '=') + break; + + index++; + } + + builder.Append (value, 0, index); + + if (index == value.Length) + return; + + int length = value.Length - index; + var buffer = ArrayPool.Shared.Rent (length * 3); + + try { + int n = Encoding.UTF8.GetBytes (value, index, length, buffer, 0); + const string HexAlphabet = "0123456789ABCDEF"; + + for (index = 0; index < n; index++) { + byte c = buffer[index]; - return MailboxAddress.EncodeAddrspec (mailbox.Address); + if (c >= 33 && c <= 126 && c != (byte) '+' && c != (byte) '=') { + builder.Append ((char) c); + } else { + builder.Append ('+'); + builder.Append (HexAlphabet[(c >> 4) & 0xF]); + builder.Append (HexAlphabet[c & 0xF]); + } + } + } finally { + ArrayPool.Shared.Return (buffer); + } + } + + [Flags] + enum SmtpExtensions + { + None = 0, + EightBitMime = 1 << 0, + BinaryMime = 1 << 1, + UTF8 = 1 << 2, } - void MailFrom (FormatOptions options, MimeMessage message, MailboxAddress mailbox, SmtpExtension extensions, CancellationToken cancellationToken) + string CreateMailFromCommand (FormatOptions options, MimeMessage message, MailboxAddress mailbox, SmtpExtensions extensions, long size) { - var utf8 = (extensions & SmtpExtension.UTF8) != 0 ? " SMTPUTF8" : string.Empty; - var addrspec = GetAddrspec (options, mailbox); + var idnEncode = (extensions & SmtpExtensions.UTF8) == 0; + var builder = new StringBuilder ("MAIL FROM:<"); + + var addrspec = mailbox.GetAddress (idnEncode); + builder.Append (addrspec); + builder.Append ('>'); + + if (!idnEncode) + builder.Append (" SMTPUTF8"); - var command = string.Format ("MAIL FROM:<{0}>{1}", addrspec, utf8); + if ((Capabilities & SmtpCapabilities.Size) != 0 && size != -1) { + builder.Append (" SIZE="); + builder.Append (size.ToString (CultureInfo.InvariantCulture)); + } - if ((extensions & SmtpExtension.BinaryMime) != 0) - command += " BODY=BINARYMIME"; - else if ((extensions & SmtpExtension.EightBitMime) != 0) - command += " BODY=8BITMIME"; + if ((extensions & SmtpExtensions.BinaryMime) != 0) + builder.Append (" BODY=BINARYMIME"); + else if ((extensions & SmtpExtensions.EightBitMime) != 0) + builder.Append (" BODY=8BITMIME"); if ((capabilities & SmtpCapabilities.Dsn) != 0) { var envid = GetEnvelopeId (message); - if (!string.IsNullOrEmpty (envid)) - command += " ENVID=" + envid; + if (!string.IsNullOrEmpty (envid)) { + builder.Append (" ENVID="); + AppendHexEncoded (builder, envid!); + } + + switch (DeliveryStatusNotificationType) { + case DeliveryStatusNotificationType.HeadersOnly: + builder.Append (" RET=HDRS"); + break; + case DeliveryStatusNotificationType.Full: + builder.Append (" RET=FULL"); + break; + } + } + + if (RequireTLS && (Capabilities & SmtpCapabilities.RequireTLS) != 0) { + // Check to see if the message has a TLS-Required header. If it does, then the only defined value it can have is "No". + var index = message.Headers.IndexOf (HeaderId.TLSRequired); + + if (index == -1) + builder.Append (" REQUIRETLS"); + } + + builder.Append ("\r\n"); + + return builder.ToString (); + } - // TODO: RET parameter? + void ParseMailFromResponse (MimeMessage message, MailboxAddress mailbox, SmtpResponse response) + { + if (response.StatusCode >= SmtpStatusCode.Ok && response.StatusCode < (SmtpStatusCode) 260) { + OnSenderAccepted (message, mailbox, response); + return; } - if ((capabilities & SmtpCapabilities.Pipelining) != 0) { + if (response.StatusCode == SmtpStatusCode.AuthenticationRequired) + throw new ServiceNotAuthenticatedException (response.Response); + + OnSenderNotAccepted (message, mailbox, response); + } + + void MailFrom (FormatOptions options, MimeMessage message, MailboxAddress mailbox, SmtpExtensions extensions, long size, bool pipeline, CancellationToken cancellationToken) + { + var command = CreateMailFromCommand (options, message, mailbox, extensions, size); + + if (pipeline) { QueueCommand (SmtpCommand.MailFrom, command, cancellationToken); return; } - var response = SendCommand (command, cancellationToken); + var response = Stream!.SendCommand (command, cancellationToken); - ProcessMailFromResponse (message, mailbox, response); + ParseMailFromResponse (message, mailbox, response); } /// @@ -1466,26 +2079,6 @@ protected virtual void OnRecipientNotAccepted (MimeMessage message, MailboxAddre throw new SmtpCommandException (SmtpErrorCode.RecipientNotAccepted, response.StatusCode, mailbox, response.Response); } - bool ProcessRcptToResponse (MimeMessage message, MailboxAddress mailbox, SmtpResponse response) - { - switch (response.StatusCode) { - case SmtpStatusCode.UserNotLocalWillForward: - case SmtpStatusCode.Ok: - OnRecipientAccepted (message, mailbox, response); - return true; - case SmtpStatusCode.UserNotLocalTryAlternatePath: - case SmtpStatusCode.MailboxNameNotAllowed: - case SmtpStatusCode.MailboxUnavailable: - case SmtpStatusCode.MailboxBusy: - OnRecipientNotAccepted (message, mailbox, response); - return false; - case SmtpStatusCode.AuthenticationRequired: - throw new ServiceNotAuthenticatedException (response.Response); - default: - throw new SmtpCommandException (SmtpErrorCode.UnexpectedStatusCode, response.StatusCode, response.Response); - } - } - /// /// Get the types of delivery status notification desired for the specified recipient mailbox. /// @@ -1497,12 +2090,33 @@ bool ProcessRcptToResponse (MimeMessage message, MailboxAddress mailbox, SmtpRes /// /// The desired delivery status notification type. /// The message being sent. - /// The mailbox. + /// The recipient mailbox. protected virtual DeliveryStatusNotification? GetDeliveryStatusNotifications (MimeMessage message, MailboxAddress mailbox) { return null; } + /// + /// Get the original intended recipient address and address type. + /// + /// + /// Gets the original intended recipient address and address type for the purpose of delivery status notification. + /// When initially submitting a message via SMTP, the address returned by this method MUST be identical to the + /// address. Likewise, when a mailing list submits a message via SMTP to be distributed to the list subscribers, the address returned by + /// this method MUST match the new RCPT TO address of each recipient, not the address specified by the original sender of the message.) + /// + /// The message being sent. + /// The recipient mailbox. + /// The original recipient address type. + /// The original recipient address. + void GetOriginalRecipientAddress (MimeMessage message, MailboxAddress mailbox, out string addrType, out string address) + { + var idnEncode = (Capabilities & SmtpCapabilities.UTF8) == 0; + + addrType = "rfc822"; + address = mailbox.GetAddress (idnEncode); + } + static string GetNotifyString (DeliveryStatusNotification notify) { string value = string.Empty; @@ -1522,34 +2136,70 @@ static string GetNotifyString (DeliveryStatusNotification notify) return value.TrimEnd (','); } - bool RcptTo (FormatOptions options, MimeMessage message, MailboxAddress mailbox, CancellationToken cancellationToken) + string CreateRcptToCommand (FormatOptions options, MimeMessage message, MailboxAddress mailbox) { - var command = string.Format ("RCPT TO:<{0}>", GetAddrspec (options, mailbox)); + var idnEncode = (Capabilities & SmtpCapabilities.UTF8) == 0; + var command = new StringBuilder ("RCPT TO:<"); + + command.Append (mailbox.GetAddress (idnEncode)); + command.Append ('>'); if ((capabilities & SmtpCapabilities.Dsn) != 0) { var notify = GetDeliveryStatusNotifications (message, mailbox); - if (notify.HasValue) - command += " NOTIFY=" + GetNotifyString (notify.Value); - } + if (notify.HasValue) { + command.Append (" NOTIFY="); + command.Append (GetNotifyString (notify.Value)); + + GetOriginalRecipientAddress (message, mailbox, out var addrType, out var address); + command.Append (" ORCPT="); + command.Append (addrType); + command.Append (';'); + AppendHexEncoded (command, address); + } + } + + command.Append ("\r\n"); + + return command.ToString (); + } + + bool ParseRcptToResponse (MimeMessage message, MailboxAddress mailbox, SmtpResponse response) + { + if (response.StatusCode < (SmtpStatusCode) 300) { + OnRecipientAccepted (message, mailbox, response); + return true; + } + + if (response.StatusCode == SmtpStatusCode.AuthenticationRequired) + throw new ServiceNotAuthenticatedException (response.Response); + + OnRecipientNotAccepted (message, mailbox, response); + + return false; + } + + bool RcptTo (FormatOptions options, MimeMessage message, MailboxAddress mailbox, bool pipeline, CancellationToken cancellationToken) + { + var command = CreateRcptToCommand (options, message, mailbox); - if ((capabilities & SmtpCapabilities.Pipelining) != 0) { + if (pipeline) { QueueCommand (SmtpCommand.RcptTo, command, cancellationToken); return false; } - var response = SendCommand (command, cancellationToken); + var response = Stream!.SendCommand (command, cancellationToken); - return ProcessRcptToResponse (message, mailbox, response); + return ParseRcptToResponse (message, mailbox, response); } class SendContext { readonly ITransferProgress progress; - readonly long? size; + readonly long size; long nwritten; - public SendContext (ITransferProgress progress, long? size) + public SendContext (ITransferProgress progress, long size) { this.progress = progress; this.size = size; @@ -1559,25 +2209,31 @@ public void Update (int n) { nwritten += n; - if (size.HasValue) - progress.Report (nwritten, size.Value); + if (size != -1) + progress.Report (nwritten, size); else progress.Report (nwritten); } } - void Bdat (FormatOptions options, MimeMessage message, CancellationToken cancellationToken, ITransferProgress progress) + string ParseBdatResponse (MimeMessage message, SmtpResponse response) { - long size; - - using (var measure = new MeasuringStream ()) { - message.WriteTo (options, measure, cancellationToken); - size = measure.Length; + switch (response.StatusCode) { + default: + throw new SmtpCommandException (SmtpErrorCode.MessageNotAccepted, response.StatusCode, response.Response); + case SmtpStatusCode.AuthenticationRequired: + throw new ServiceNotAuthenticatedException (response.Response); + case SmtpStatusCode.Ok: + OnMessageSent (new MessageSentEventArgs (message, response.Response)); + return response.Response; } + } - var bytes = Encoding.UTF8.GetBytes (string.Format ("BDAT {0} LAST\r\n", size)); + string Bdat (FormatOptions options, MimeMessage message, long size, CancellationToken cancellationToken, ITransferProgress? progress) + { + var command = string.Format (CultureInfo.InvariantCulture, "BDAT {0} LAST\r\n", size); - Stream.Write (bytes, 0, bytes.Length, cancellationToken); + Stream!.QueueCommand (command, cancellationToken); if (progress != null) { var ctx = new SendContext (progress, size); @@ -1593,6 +2249,17 @@ void Bdat (FormatOptions options, MimeMessage message, CancellationToken cancell var response = Stream.ReadResponse (cancellationToken); + return ParseBdatResponse (message, response); + } + + static void ParseDataResponse (SmtpResponse response) + { + if (response.StatusCode != SmtpStatusCode.StartMailInput) + throw new SmtpCommandException (SmtpErrorCode.UnexpectedStatusCode, response.StatusCode, response.Response); + } + + string ParseMessageDataResponse (MimeMessage message, SmtpResponse response) + { switch (response.StatusCode) { default: throw new SmtpCommandException (SmtpErrorCode.MessageNotAccepted, response.StatusCode, response.Response); @@ -1600,67 +2267,109 @@ void Bdat (FormatOptions options, MimeMessage message, CancellationToken cancell throw new ServiceNotAuthenticatedException (response.Response); case SmtpStatusCode.Ok: OnMessageSent (new MessageSentEventArgs (message, response.Response)); - break; + return response.Response; } } - void Data (FormatOptions options, MimeMessage message, CancellationToken cancellationToken, ITransferProgress progress) + string MessageData (FormatOptions options, MimeMessage message, long size, CancellationToken cancellationToken, ITransferProgress? progress) { - var response = SendCommand ("DATA", cancellationToken); - - if (response.StatusCode != SmtpStatusCode.StartMailInput) - throw new SmtpCommandException (SmtpErrorCode.UnexpectedStatusCode, response.StatusCode, response.Response); - if (progress != null) { - var ctx = new SendContext (progress, null); + var ctx = new SendContext (progress, size); - using (var stream = new ProgressStream (Stream, ctx.Update)) { + using (var stream = new ProgressStream (Stream!, ctx.Update)) { using (var filtered = new FilteredStream (stream)) { filtered.Add (new SmtpDataFilter ()); message.WriteTo (options, filtered, cancellationToken); - filtered.Flush (); + filtered.Flush (cancellationToken); } } } else { - using (var filtered = new FilteredStream (Stream)) { + using (var filtered = new FilteredStream (Stream!)) { filtered.Add (new SmtpDataFilter ()); message.WriteTo (options, filtered, cancellationToken); - filtered.Flush (); + filtered.Flush (cancellationToken); } } - Stream.Write (EndData, 0, EndData.Length, cancellationToken); + Stream!.Write (EndData, 0, EndData.Length, cancellationToken); Stream.Flush (cancellationToken); - response = Stream.ReadResponse (cancellationToken); + var response = Stream.ReadResponse (cancellationToken); - switch (response.StatusCode) { - default: - throw new SmtpCommandException (SmtpErrorCode.MessageNotAccepted, response.StatusCode, response.Response); - case SmtpStatusCode.AuthenticationRequired: - throw new ServiceNotAuthenticatedException (response.Response); - case SmtpStatusCode.Ok: - OnMessageSent (new MessageSentEventArgs (message, response.Response)); - break; - } + return ParseMessageDataResponse (message, response); } void Reset (CancellationToken cancellationToken) { + SmtpResponse response; + try { - var response = SendCommand ("RSET", cancellationToken); - if (response.StatusCode != SmtpStatusCode.Ok) - Disconnect (false, cancellationToken); - } catch (SmtpCommandException) { - // do not disconnect + response = SendCommandInternal ("RSET\r\n", cancellationToken); } catch { - Disconnect (); + // Swallow RSET exceptions so that we do not obscure the exception that caused the need for the RSET command in the first place. + return; + } + + if (response.StatusCode != SmtpStatusCode.Ok) + Disconnect (uri!.Host, uri.Port, GetSecureSocketOptions (uri), false); + } + + /// + /// Prepare the message for transport with the specified constraints. + /// + /// + /// Prepares the message for transport with the specified constraints. + /// Typically, this involves calling on + /// the message with the provided constraints. + /// + /// The format options. + /// The message. + /// The encoding constraint. + /// The max line length supported by the server. + protected virtual void Prepare (FormatOptions options, MimeMessage message, EncodingConstraint constraint, int maxLineLength) + { + if (!message.Headers.Contains (HeaderId.DomainKeySignature) && + !message.Headers.Contains (HeaderId.DkimSignature) && + !message.Headers.Contains (HeaderId.ArcSeal)) { + // prepare the message + message.Prepare (constraint, maxLineLength); + } else { + // Note: we do not want to risk reformatting of headers to the international + // UTF-8 encoding, so disable it. + options.International = false; + } + } + + /// + /// Get the size of the message. + /// + /// + /// Calculates the size of the message in bytes. + /// This method is called by Send + /// methods in the following conditions: + /// + /// The SMTP server supports the SIZE= parameter in the MAIL FROM command. + /// The parameter is non-null. + /// The SMTP server supports the CHUNKING extension. + /// + /// + /// The size of the message, in bytes. + /// The formatting options. + /// The message. + /// The cancellation token. + protected virtual long GetSize (FormatOptions options, MimeMessage message, CancellationToken cancellationToken) + { + using (var measure = new MeasuringStream ()) { + message.WriteTo (options, measure, cancellationToken); + + return measure.Length; } } - void Send (FormatOptions options, MimeMessage message, MailboxAddress sender, IList recipients, CancellationToken cancellationToken, ITransferProgress progress) + [MemberNotNull (nameof (Stream), nameof (uri))] + FormatOptions Prepare (FormatOptions options, MimeMessage message, MailboxAddress sender, IList recipients, out SmtpExtensions extensions) { CheckDisposed (); @@ -1668,10 +2377,8 @@ void Send (FormatOptions options, MimeMessage message, MailboxAddress sender, IL throw new ServiceNotConnectedException ("The SmtpClient is not connected."); var format = options.Clone (); - format.HiddenHeaders.Add (HeaderId.ContentLength); - format.HiddenHeaders.Add (HeaderId.ResentBcc); - format.HiddenHeaders.Add (HeaderId.Bcc); format.NewLineFormat = NewLineFormat.Dos; + format.EnsureNewLine = true; if (format.International && (Capabilities & SmtpCapabilities.UTF8) == 0) format.International = false; @@ -1679,59 +2386,146 @@ void Send (FormatOptions options, MimeMessage message, MailboxAddress sender, IL if (format.International && (Capabilities & SmtpCapabilities.EightBitMime) == 0) throw new NotSupportedException ("The SMTP server does not support the 8BITMIME extension."); - // prepare the message + EncodingConstraint constraint; + if ((Capabilities & SmtpCapabilities.BinaryMime) != 0) - message.Prepare (EncodingConstraint.None, MaxLineLength); + constraint = EncodingConstraint.None; else if ((Capabilities & SmtpCapabilities.EightBitMime) != 0) - message.Prepare (EncodingConstraint.EightBit, MaxLineLength); + constraint = EncodingConstraint.EightBit; else - message.Prepare (EncodingConstraint.SevenBit, MaxLineLength); + constraint = EncodingConstraint.SevenBit; + + Prepare (format, message, constraint, MaxLineLength); // figure out which SMTP extensions we need to use - var visitor = new ContentTransferEncodingVisitor (capabilities); - visitor.Visit (message); + extensions = SmtpExtensions.None; + + using (var iter = new MimeIterator (message)) { + while (iter.MoveNext ()) { + if (iter.Current is MimePart part) { + if (part.ContentTransferEncoding == ContentEncoding.EightBit) { + if ((capabilities & SmtpCapabilities.EightBitMime) != 0) { + extensions |= SmtpExtensions.EightBitMime; + + if ((capabilities & SmtpCapabilities.BinaryMime) == 0) { + // BINARYMIME is not supported, so there's no sense in continuing to scan more MimeParts. + break; + } + } + } else if (part.ContentTransferEncoding == ContentEncoding.Binary) { + if ((capabilities & SmtpCapabilities.BinaryMime) != 0) { + // Once we've decided we require BINARYMIME, no sense continuing to check for 8BITMIME. + extensions |= SmtpExtensions.BinaryMime; + break; + } + } + } + } + } + + if ((Capabilities & SmtpCapabilities.UTF8) != 0 && (format.International || sender.IsInternational || recipients.Any (x => x.IsInternational))) + extensions |= SmtpExtensions.UTF8; + + return format; + } + + [MethodImpl (MethodImplOptions.AggressiveInlining)] + bool UseBdatCommand (SmtpExtensions extensions) + { + return (extensions & SmtpExtensions.BinaryMime) != 0 || (PreferSendAsBinaryData && (Capabilities & (SmtpCapabilities.BinaryMime | SmtpCapabilities.Chunking)) != 0); + } - var extensions = visitor.SmtpExtensions; + string Send (FormatOptions options, MimeMessage message, MailboxAddress sender, IList recipients, CancellationToken cancellationToken, ITransferProgress? progress) + { + var format = Prepare (options, message, sender, recipients, out var extensions); + var pipeline = (capabilities & SmtpCapabilities.Pipelining) != 0; + var bdat = UseBdatCommand (extensions); + long size; + + if (bdat || (Capabilities & SmtpCapabilities.Size) != 0 || progress != null) { + size = GetSize (format, message, cancellationToken); + } else { + size = -1; + } - if (format.International) - extensions |= SmtpExtension.UTF8; + using var operation = StartNetworkOperation (NetworkOperationKind.Send); try { // Note: if PIPELINING is supported, MailFrom() and RcptTo() will // queue their commands instead of sending them immediately. - MailFrom (format, message, sender, extensions, cancellationToken); + MailFrom (format, message, sender, extensions, size, pipeline, cancellationToken); - int accepted = 0; + int recipientsAccepted = 0; for (int i = 0; i < recipients.Count; i++) { - if (RcptTo (format, message, recipients[i], cancellationToken)) - accepted++; + if (RcptTo (format, message, recipients[i], pipeline, cancellationToken)) + recipientsAccepted++; } if (queued.Count > 0) { // Note: if PIPELINING is supported, this will flush all outstanding - // MAIL FROM and RCPT TO commands to the server and then process all - // of their responses. - FlushCommandQueue (message, sender, recipients, cancellationToken); - } else if (accepted == 0) { + // MAIL FROM and RCPT TO commands to the server and then process + // all of their responses. + var results = FlushCommandQueue (message, sender, recipients, cancellationToken); + + recipientsAccepted = results.RecipientsAccepted; + + if (results.FirstException != null) + throw results.FirstException; + } + + if (recipientsAccepted == 0) { OnNoRecipientsAccepted (message); + throw new SmtpCommandException (SmtpErrorCode.MessageNotAccepted, SmtpStatusCode.TransactionFailed, "No recipients were accepted."); } - if ((extensions & SmtpExtension.BinaryMime) != 0) - Bdat (format, message, cancellationToken, progress); - else - Data (format, message, cancellationToken, progress); - } catch (ServiceNotAuthenticatedException) { + if (bdat) + return Bdat (format, message, size, cancellationToken, progress); + + var dataResponse = Stream.SendCommand ("DATA\r\n", cancellationToken); + + ParseDataResponse (dataResponse); + + return MessageData (format, message, size, cancellationToken, progress); + } catch (ServiceNotAuthenticatedException ex) { + operation.SetError (ex); + // do not disconnect + Reset (cancellationToken); throw; - } catch (SmtpCommandException) { + } catch (SmtpCommandException ex) { + operation.SetError (ex); + + // do not disconnect Reset (cancellationToken); throw; - } catch { - Disconnect (); + } catch (Exception ex) { + operation.SetError (ex); + + Disconnect (uri.Host, uri.Port, GetSecureSocketOptions (uri), false); throw; } } + static void ValidateArguments (FormatOptions options, MimeMessage message, out MailboxAddress sender, out IList recipients) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (message == null) + throw new ArgumentNullException (nameof (message)); + + var mailbox = GetMessageSender (message); + + if (mailbox == null) + throw new InvalidOperationException ("No sender has been specified."); + + sender = mailbox; + recipients = GetMessageRecipients (message); + + if (recipients.Count == 0) + throw new InvalidOperationException ("No recipients have been specified."); + } + /// /// Send the specified message. /// @@ -1747,14 +2541,15 @@ void Send (FormatOptions options, MimeMessage message, MailboxAddress sender, IL /// /// /// + /// The final free-form text response from the server. /// The formatting options. /// The message. /// The cancellation token. /// The progress reporting mechanism. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// The has been disposed. @@ -1785,7 +2580,14 @@ void Send (FormatOptions options, MimeMessage message, MailboxAddress sender, IL /// /// An SMTP protocol exception occurred. /// - public override void Send (FormatOptions options, MimeMessage message, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + public override string Send (FormatOptions options, MimeMessage message, CancellationToken cancellationToken = default, ITransferProgress? progress = null) + { + ValidateArguments (options, message, out var sender, out var recipients); + + return Send (options, message, sender, recipients, cancellationToken, progress); + } + + static List ValidateArguments (FormatOptions options, MimeMessage message, MailboxAddress sender, IEnumerable recipients) { if (options == null) throw new ArgumentNullException (nameof (options)); @@ -1793,16 +2595,21 @@ void Send (FormatOptions options, MimeMessage message, MailboxAddress sender, IL if (message == null) throw new ArgumentNullException (nameof (message)); - var recipients = GetMessageRecipients (message); - var sender = GetMessageSender (message); - if (sender == null) - throw new InvalidOperationException ("No sender has been specified."); + throw new ArgumentNullException (nameof (sender)); - if (recipients.Count == 0) + if (recipients == null) + throw new ArgumentNullException (nameof (recipients)); + + var unique = new HashSet (StringComparer.OrdinalIgnoreCase); + var rcpts = new List (); + + AddUnique (rcpts, unique, recipients); + + if (rcpts.Count == 0) throw new InvalidOperationException ("No recipients have been specified."); - Send (options, message, sender, recipients, cancellationToken, progress); + return rcpts; } /// @@ -1811,6 +2618,7 @@ void Send (FormatOptions options, MimeMessage message, MailboxAddress sender, IL /// /// Sends the message by uploading it to an SMTP server using the supplied sender and recipients. /// + /// The final free-form text response from the server. /// The formatting options. /// The message. /// The mailbox address to use for sending the message. @@ -1818,13 +2626,13 @@ void Send (FormatOptions options, MimeMessage message, MailboxAddress sender, IL /// The cancellation token. /// The progress reporting mechanism. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// -or- - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// The has been disposed. @@ -1855,70 +2663,16 @@ void Send (FormatOptions options, MimeMessage message, MailboxAddress sender, IL /// /// An SMTP protocol exception occurred. /// - public override void Send (FormatOptions options, MimeMessage message, MailboxAddress sender, IEnumerable recipients, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + public override string Send (FormatOptions options, MimeMessage message, MailboxAddress sender, IEnumerable recipients, CancellationToken cancellationToken = default, ITransferProgress? progress = null) { - if (options == null) - throw new ArgumentNullException (nameof (options)); - - if (message == null) - throw new ArgumentNullException (nameof (message)); - - if (sender == null) - throw new ArgumentNullException (nameof (sender)); - - if (recipients == null) - throw new ArgumentNullException (nameof (recipients)); - - var unique = new HashSet (StringComparer.OrdinalIgnoreCase); - var rcpts = new List (); - - AddUnique (rcpts, unique, recipients); - - if (rcpts.Count == 0) - throw new InvalidOperationException ("No recipients have been specified."); + var rcpts = ValidateArguments (options, message, sender, recipients); - Send (options, message, sender, rcpts, cancellationToken, progress); + return Send (options, message, sender, rcpts, cancellationToken, progress); } #endregion - /// - /// Expand a mailing address alias. - /// - /// - /// Expands a mailing address alias. - /// - /// The expanded list of mailbox addresses. - /// The mailing address alias. - /// The cancellation token. - /// - /// is null. - /// - /// - /// is an empty string. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// Authentication is required before verifying the existence of an address. - /// - /// - /// The operation has been canceled. - /// - /// - /// An I/O error occurred. - /// - /// - /// The SMTP command failed. - /// - /// - /// An SMTP protocol exception occurred. - /// - public InternetAddressList Expand (string alias, CancellationToken cancellationToken = default (CancellationToken)) + string CreateExpandCommand (string alias) { if (alias == null) throw new ArgumentNullException (nameof (alias)); @@ -1926,7 +2680,7 @@ void Send (FormatOptions options, MimeMessage message, MailboxAddress sender, IL if (alias.Length == 0) throw new ArgumentException ("The alias cannot be empty.", nameof (alias)); - if (alias.IndexOfAny (new [] { '\r', '\n' }) != -1) + if (alias.IndexOfAny (NewLineCharacters) != -1) throw new ArgumentException ("The alias cannot contain newline characters.", nameof (alias)); CheckDisposed (); @@ -1934,8 +2688,11 @@ void Send (FormatOptions options, MimeMessage message, MailboxAddress sender, IL if (!IsConnected) throw new ServiceNotConnectedException ("The SmtpClient is not connected."); - var response = SendCommand (string.Format ("EXPN {0}", alias), cancellationToken); + return string.Format ("EXPN {0}\r\n", alias); + } + static InternetAddressList ParseExpandResponse (SmtpResponse response) + { if (response.StatusCode != SmtpStatusCode.Ok) throw new SmtpCommandException (SmtpErrorCode.UnexpectedStatusCode, response.StatusCode, response.Response); @@ -1943,9 +2700,7 @@ void Send (FormatOptions options, MimeMessage message, MailboxAddress sender, IL var list = new InternetAddressList (); for (int i = 0; i < lines.Length; i++) { - InternetAddress address; - - if (InternetAddress.TryParse (lines[i], out address)) + if (InternetAddress.TryParse (lines[i], out var address)) list.Add (address); } @@ -1953,16 +2708,19 @@ void Send (FormatOptions options, MimeMessage message, MailboxAddress sender, IL } /// - /// Asynchronously expand a mailing address alias. + /// Expand a mailing address alias. /// /// - /// Asynchronously expands a mailing address alias. + /// Expands a mailing address alias. /// + /// + /// + /// /// The expanded list of mailbox addresses. /// The mailing address alias. /// The cancellation token. /// - /// is null. + /// is . /// /// /// is an empty string. @@ -1974,7 +2732,7 @@ void Send (FormatOptions options, MimeMessage message, MailboxAddress sender, IL /// The is not connected. /// /// - /// Authentication is required before verifying the existence of an address. + /// Authentication is required before expanding an alias. /// /// /// The operation has been canceled. @@ -1988,62 +2746,14 @@ void Send (FormatOptions options, MimeMessage message, MailboxAddress sender, IL /// /// An SMTP protocol exception occurred. /// - public Task ExpandAsync (string alias, CancellationToken cancellationToken = default (CancellationToken)) + public InternetAddressList Expand (string alias, CancellationToken cancellationToken = default) { - if (alias == null) - throw new ArgumentNullException (nameof (alias)); - - if (alias.Length == 0) - throw new ArgumentException ("The alias cannot be empty.", nameof (alias)); - - if (alias.IndexOfAny (new [] { '\r', '\n' }) != -1) - throw new ArgumentException ("The alias cannot contain newline characters.", nameof (alias)); + var response = SendCommandInternal (CreateExpandCommand (alias), cancellationToken); - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return Expand (alias, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); + return ParseExpandResponse (response); } - /// - /// Verify the existence of a mailbox address. - /// - /// - /// Verifies the existence a mailbox address with the SMTP server, returning the expanded - /// mailbox address if it exists. - /// - /// The expanded mailbox address. - /// The mailbox address. - /// The cancellation token. - /// - /// is null. - /// - /// - /// is an empty string. - /// - /// - /// The has been disposed. - /// - /// - /// The is not connected. - /// - /// - /// Authentication is required before verifying the existence of an address. - /// - /// - /// The operation has been canceled. - /// - /// - /// An I/O error occurred. - /// - /// - /// The SMTP command failed. - /// - /// - /// An SMTP protocol exception occurred. - /// - public MailboxAddress Verify (string address, CancellationToken cancellationToken = default (CancellationToken)) + string CreateVerifyCommand (string address) { if (address == null) throw new ArgumentNullException (nameof (address)); @@ -2051,7 +2761,7 @@ void Send (FormatOptions options, MimeMessage message, MailboxAddress sender, IL if (address.Length == 0) throw new ArgumentException ("The address cannot be empty.", nameof (address)); - if (address.IndexOfAny (new [] { '\r', '\n' }) != -1) + if (address.IndexOfAny (NewLineCharacters) != -1) throw new ArgumentException ("The address cannot contain newline characters.", nameof (address)); CheckDisposed (); @@ -2059,8 +2769,11 @@ void Send (FormatOptions options, MimeMessage message, MailboxAddress sender, IL if (!IsConnected) throw new ServiceNotConnectedException ("The SmtpClient is not connected."); - var response = SendCommand (string.Format ("VRFY {0}", address), cancellationToken); + return string.Format ("VRFY {0}\r\n", address); + } + static MailboxAddress ParseVerifyResponse (SmtpResponse response) + { if (response.StatusCode == SmtpStatusCode.Ok) return MailboxAddress.Parse (response.Response); @@ -2068,17 +2781,20 @@ void Send (FormatOptions options, MimeMessage message, MailboxAddress sender, IL } /// - /// Asynchronously verify the existence of a mailbox address. + /// Verify the existence of a mailbox address. /// /// - /// Asynchronously verifies the existence a mailbox address with the SMTP server, - /// returning the expanded mailbox address if it exists. + /// Verifies the existence a mailbox address with the SMTP server, returning the expanded + /// mailbox address if it exists. /// + /// + /// + /// /// The expanded mailbox address. /// The mailbox address. /// The cancellation token. /// - /// is null. + /// is . /// /// /// is an empty string. @@ -2104,22 +2820,11 @@ void Send (FormatOptions options, MimeMessage message, MailboxAddress sender, IL /// /// An SMTP protocol exception occurred. /// - public Task VerifyAsync (string address, CancellationToken cancellationToken = default (CancellationToken)) + public MailboxAddress Verify (string address, CancellationToken cancellationToken = default) { - if (address == null) - throw new ArgumentNullException (nameof (address)); - - if (address.Length == 0) - throw new ArgumentException ("The address cannot be empty.", nameof (address)); - - if (address.IndexOfAny (new [] { '\r', '\n' }) != -1) - throw new ArgumentException ("The address cannot contain newline characters.", nameof (address)); + var response = SendCommandInternal (CreateVerifyCommand (address), cancellationToken); - return Task.Factory.StartNew (() => { - lock (SyncRoot) { - return Verify (address, cancellationToken); - } - }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); + return ParseVerifyResponse (response); } /// @@ -2130,13 +2835,13 @@ void Send (FormatOptions options, MimeMessage message, MailboxAddress sender, IL /// Releases the unmanaged resources used by the and /// optionally releases the managed resources. /// - /// true to release both managed and unmanaged resources; - /// false to release only the unmanaged resources. + /// to release both managed and unmanaged resources; + /// to release only the unmanaged resources. protected override void Dispose (bool disposing) { if (disposing && !disposed) { disposed = true; - Disconnect (); + Disconnect (null, 0, SecureSocketOptions.None, false); } base.Dispose (disposed); diff --git a/MailKit/Net/Smtp/SmtpCommandException.cs b/MailKit/Net/Smtp/SmtpCommandException.cs index 7b7c7dfe7b..0019a9fd51 100644 --- a/MailKit/Net/Smtp/SmtpCommandException.cs +++ b/MailKit/Net/Smtp/SmtpCommandException.cs @@ -1,9 +1,9 @@ -// +// // SmtpCommandException.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -33,44 +33,6 @@ using MimeKit; namespace MailKit.Net.Smtp { - /// - /// An enumeration of the possible error codes that may be reported by a . - /// - /// - /// An enumeration of the possible error codes that may be reported by a . - /// - /// - /// - /// - public enum SmtpErrorCode { - /// - /// The message was not accepted for delivery. This may happen if - /// the server runs out of available disk space. - /// - MessageNotAccepted, - - /// - /// The sender's mailbox address was not accepted. Check the - /// property for the - /// mailbox used as the sender's mailbox address. - /// - SenderNotAccepted, - - /// - /// A recipient's mailbox address was not accepted. Check the - /// property for the - /// particular recipient mailbox that was not acccepted. - /// - RecipientNotAccepted, - - /// - /// An unexpected status code was returned by the server. - /// For more details, the - /// property may provide some additional hints. - /// - UnexpectedStatusCode, - } - /// /// An SMTP protocol exception. /// @@ -96,16 +58,15 @@ public class SmtpCommandException : CommandException /// The serialization info. /// The streaming context. /// - /// is null. + /// is . /// [SecuritySafeCritical] + [Obsolete ("This API supports obsolete formatter-based serialization. It should not be called or extended by application code.")] protected SmtpCommandException (SerializationInfo info, StreamingContext context) : base (info, context) { - MailboxAddress mailbox; - string value; + var value = info.GetString ("Mailbox"); - value = info.GetString ("Mailbox"); - if (!string.IsNullOrEmpty (value) && MailboxAddress.TryParse (value, out mailbox)) + if (!string.IsNullOrEmpty (value) && MailboxAddress.TryParse (value, out var mailbox)) Mailbox = mailbox; ErrorCode = (SmtpErrorCode) info.GetValue ("ErrorCode", typeof (SmtpErrorCode)); @@ -113,6 +74,24 @@ protected SmtpCommandException (SerializationInfo info, StreamingContext context } #endif + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The error code. + /// The status code. + /// The rejected mailbox. + /// The error message. + /// The inner exception. + public SmtpCommandException (SmtpErrorCode code, SmtpStatusCode status, MailboxAddress mailbox, string message, Exception innerException) : base (message, innerException) + { + StatusCode = status; + Mailbox = mailbox; + ErrorCode = code; + } + /// /// Initializes a new instance of the class. /// @@ -130,6 +109,22 @@ public SmtpCommandException (SmtpErrorCode code, SmtpStatusCode status, MailboxA ErrorCode = code; } + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The error code. + /// The status code.> + /// The error message. + /// The inner exception. + public SmtpCommandException (SmtpErrorCode code, SmtpStatusCode status, string message, Exception innerException) : base (message, innerException) + { + StatusCode = status; + ErrorCode = code; + } + /// /// Initializes a new instance of the class. /// @@ -156,9 +151,12 @@ public SmtpCommandException (SmtpErrorCode code, SmtpStatusCode status, string m /// The serialization info. /// The streaming context. /// - /// is null. + /// is . /// [SecurityCritical] +#if NET8_0_OR_GREATER + [Obsolete ("This API supports obsolete formatter-based serialization. It should not be called or extended by application code.")] +#endif public override void GetObjectData (SerializationInfo info, StreamingContext context) { base.GetObjectData (info, context); @@ -174,10 +172,10 @@ public override void GetObjectData (SerializationInfo info, StreamingContext con #endif /// - /// Gets the error code which may provide additional information. + /// Get the error code which may provide additional information. /// /// - /// The error code can be used to programatically deal with the + /// The error code can be used to programmatically deal with the /// exception without necessarily needing to display the raw /// exception message to the user. /// @@ -190,7 +188,7 @@ public SmtpErrorCode ErrorCode { } /// - /// Gets the mailbox that the error occurred on. + /// Get the mailbox that the error occurred on. /// /// /// This property will only be available when the @@ -202,12 +200,12 @@ public SmtpErrorCode ErrorCode { /// /// /// The mailbox. - public MailboxAddress Mailbox { + public MailboxAddress? Mailbox { get; private set; } /// - /// Gets the status code returned by the SMTP server. + /// Get the status code returned by the SMTP server. /// /// /// The raw SMTP status code that resulted in the diff --git a/MailKit/Net/Smtp/SmtpDataFilter.cs b/MailKit/Net/Smtp/SmtpDataFilter.cs index 971fcc07db..09b6cb7765 100644 --- a/MailKit/Net/Smtp/SmtpDataFilter.cs +++ b/MailKit/Net/Smtp/SmtpDataFilter.cs @@ -1,9 +1,9 @@ -// +// // SmtpDataFilter.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -28,28 +28,45 @@ namespace MailKit.Net.Smtp { /// - /// A special stream filter that escapes lines beginning with a '.' - /// as needed when uploading a message to an SMTP server. + /// An SMTP filter designed to format a message stream for the DATA command. /// - class SmtpDataFilter : MimeFilterBase + /// + /// A special stream filter that can encode or decode lines beginning with a '.' as + /// needed when sending/receiving a message via the SMTP protocol or when saving a + /// message to an IIS message pickup directory. + /// + /// + /// + /// + /// + public class SmtpDataFilter : MimeFilterBase { - bool bol = true; + readonly bool decode; + bool bol; /// - /// Filter the specified input. + /// Initializes a new instance of the class. /// - /// The filtered output. - /// The input buffer. - /// The starting index of the input buffer. - /// Length. - /// Output index. - /// Output length. - /// If set to true flush. - protected override byte[] Filter (byte[] input, int startIndex, int length, out int outputIndex, out int outputLength, bool flush) + /// + /// Creates a new . + /// + /// if the filter should decode the content; otherwise, . + /// + /// + /// + /// + public SmtpDataFilter (bool decode = false) + { + this.decode = decode; + bol = true; + } + + byte[] Encode (byte[] input, int startIndex, int length, out int outputIndex, out int outputLength, bool flush) { int inputEnd = startIndex + length; bool escape = bol; int ndots = 0; + int crlf = 0; for (int i = startIndex; i < inputEnd; i++) { byte c = input[i]; @@ -62,7 +79,17 @@ protected override byte[] Filter (byte[] input, int startIndex, int length, out } } - EnsureOutputSize (length + ndots, false); + if (flush && !escape) + crlf = 2; + + if (ndots + crlf == 0) { + outputIndex = startIndex; + outputLength = length; + bol = escape; + return input; + } + + EnsureOutputSize (length + ndots + crlf, false); int index = 0; for (int i = startIndex; i < inputEnd; i++) { @@ -78,15 +105,68 @@ protected override byte[] Filter (byte[] input, int startIndex, int length, out OutputBuffer[index++] = c; } + if (crlf > 0) { + OutputBuffer[index++] = (byte) '\r'; + OutputBuffer[index++] = (byte) '\n'; + } + outputLength = index; outputIndex = 0; return OutputBuffer; } + byte[] Decode (byte[] input, int startIndex, int length, out int outputIndex, out int outputLength, bool flush) + { + int inputEnd = startIndex + length; + int index = startIndex; + + EnsureOutputSize (length, false); + outputLength = 0; + outputIndex = 0; + + while (index < inputEnd) { + byte c = input[index++]; + + if (bol && c == (byte) '.') { + bol = false; + } else { + OutputBuffer[outputLength++] = c; + bol = c == (byte) '\n'; + } + } + + return OutputBuffer; + } + /// - /// Resets the filter. + /// Filter the specified input. /// + /// + /// Filters the specified input buffer starting at the given index, + /// spanning across the specified number of bytes. + /// + /// The filtered output. + /// The input buffer. + /// The starting index of the input buffer. + /// The length of the input buffer, starting at . + /// The output index. + /// The output length. + /// If set to , all internally buffered data should be flushed to the output buffer. + protected override byte[] Filter (byte[] input, int startIndex, int length, out int outputIndex, out int outputLength, bool flush) + { + if (decode) + return Decode (input, startIndex, length, out outputIndex, out outputLength, flush); + + return Encode (input, startIndex, length, out outputIndex, out outputLength, flush); + } + + /// + /// Reset the filter. + /// + /// + /// Resets the filter. + /// public override void Reset () { base.Reset (); diff --git a/MailKit/Net/Smtp/SmtpErrorCode.cs b/MailKit/Net/Smtp/SmtpErrorCode.cs new file mode 100644 index 0000000000..047da0b363 --- /dev/null +++ b/MailKit/Net/Smtp/SmtpErrorCode.cs @@ -0,0 +1,68 @@ +// +// SmtpErrorCode.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MailKit.Net.Smtp { + /// + /// An enumeration of the possible error codes that may be reported by a . + /// + /// + /// An enumeration of the possible error codes that may be reported by a . + /// + /// + /// + /// + public enum SmtpErrorCode + { + /// + /// The message was not accepted for delivery. This may happen if + /// the server runs out of available disk space. + /// + MessageNotAccepted, + + /// + /// The sender's mailbox address was not accepted. Check the + /// property for the + /// mailbox used as the sender's mailbox address. + /// + SenderNotAccepted, + + /// + /// A recipient's mailbox address was not accepted. Check the + /// property for the + /// particular recipient mailbox that was not accepted. + /// + RecipientNotAccepted, + + /// + /// An unexpected status code was returned by the server. + /// For more details, the + /// property may provide some additional hints. + /// + UnexpectedStatusCode, + } +} diff --git a/MailKit/Net/Smtp/SmtpProtocolException.cs b/MailKit/Net/Smtp/SmtpProtocolException.cs index a55571343d..dbf0956bd8 100644 --- a/MailKit/Net/Smtp/SmtpProtocolException.cs +++ b/MailKit/Net/Smtp/SmtpProtocolException.cs @@ -1,9 +1,9 @@ -// +// // SmtpProtocolException.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -57,9 +57,10 @@ public class SmtpProtocolException : ProtocolException /// The serialization info. /// The streaming context. /// - /// is null. + /// is . /// [SecuritySafeCritical] + [Obsolete ("This API supports obsolete formatter-based serialization. It should not be called or extended by application code.")] protected SmtpProtocolException (SerializationInfo info, StreamingContext context) : base (info, context) { } diff --git a/MailKit/Net/Smtp/SmtpResponse.cs b/MailKit/Net/Smtp/SmtpResponse.cs index 76ba7bda77..e17b711c6a 100644 --- a/MailKit/Net/Smtp/SmtpResponse.cs +++ b/MailKit/Net/Smtp/SmtpResponse.cs @@ -1,9 +1,9 @@ -// +// // SmtpResponse.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal diff --git a/MailKit/Net/Smtp/SmtpStatusCode.cs b/MailKit/Net/Smtp/SmtpStatusCode.cs index aa6c8cdb05..c74a790d33 100644 --- a/MailKit/Net/Smtp/SmtpStatusCode.cs +++ b/MailKit/Net/Smtp/SmtpStatusCode.cs @@ -1,9 +1,9 @@ -// +// // SmtpStatusCode.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal diff --git a/MailKit/Net/Smtp/SmtpStream.cs b/MailKit/Net/Smtp/SmtpStream.cs index eb27995c14..5339a6fe11 100644 --- a/MailKit/Net/Smtp/SmtpStream.cs +++ b/MailKit/Net/Smtp/SmtpStream.cs @@ -1,9 +1,9 @@ -// +// // SmtpStream.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -28,20 +28,8 @@ using System.IO; using System.Text; using System.Threading; -using Buffer = System.Buffer; - -#if NETFX_CORE -using Windows.Storage.Streams; -using Windows.Networking.Sockets; -using Encoding = Portable.Text.Encoding; -using Socket = Windows.Networking.Sockets.StreamSocket; -using EncoderExceptionFallback = Portable.Text.EncoderExceptionFallback; -using DecoderExceptionFallback = Portable.Text.DecoderExceptionFallback; -using DecoderFallbackException = Portable.Text.DecoderFallbackException; -#else -using System.Net.Security; using System.Net.Sockets; -#endif +using System.Threading.Tasks; using MimeKit.IO; @@ -54,35 +42,19 @@ namespace MailKit.Net.Smtp { /// class SmtpStream : Stream, ICancellableStream { - static readonly Encoding Latin1; - static readonly Encoding UTF8; - const int ReadAheadSize = 128; const int BlockSize = 4096; const int PadSize = 4; // I/O buffering - readonly byte[] input = new byte[ReadAheadSize + BlockSize + PadSize]; - const int inputStart = ReadAheadSize; - + readonly byte[] input = new byte[BlockSize + PadSize]; readonly byte[] output = new byte[BlockSize]; int outputIndex; readonly IProtocolLogger logger; - int inputIndex = ReadAheadSize; - int inputEnd = ReadAheadSize; + int inputIndex, inputEnd; + string? lastResponse; bool disposed; - static SmtpStream () - { - UTF8 = Encoding.GetEncoding (65001, new EncoderExceptionFallback (), new DecoderExceptionFallback ()); - - try { - Latin1 = Encoding.GetEncoding (28591); - } catch (NotSupportedException) { - Latin1 = Encoding.GetEncoding (1252); - } - } - /// /// Initializes a new instance of the class. /// @@ -90,36 +62,32 @@ static SmtpStream () /// Creates a new . /// /// The underlying network stream. - /// The underlying network socket. /// The protocol logger. - public SmtpStream (Stream source, Socket socket, IProtocolLogger protocolLogger) + public SmtpStream (Stream source, IProtocolLogger protocolLogger) { logger = protocolLogger; IsConnected = true; Stream = source; - Socket = socket; } /// - /// Get or sets the underlying network stream. + /// Get the underlying network stream. /// /// - /// Gets or sets the underlying network stream. + /// Gets the underlying network stream. /// /// The underlying network stream. public Stream Stream { - get; internal set; + get; private set; } - /// - /// Get the underlying network socket. - /// - /// - /// Gets the underlying network socket. - /// - /// The underlying network socket. - public Socket Socket { - get; private set; + internal void SetStream (Stream stream) + { + Stream = stream; + + // reset internal buffering + inputIndex = 0; + inputEnd = 0; } /// @@ -128,7 +96,7 @@ public Socket Socket { /// /// Gets whether or not the stream is connected. /// - /// true if the stream is connected; otherwise, false. + /// if the stream is connected; otherwise, . public bool IsConnected { get; private set; } @@ -139,7 +107,7 @@ public bool IsConnected { /// /// Gets whether the stream supports reading. /// - /// true if the stream supports reading; otherwise, false. + /// if the stream supports reading; otherwise, . public override bool CanRead { get { return Stream.CanRead; } } @@ -150,7 +118,7 @@ public override bool CanRead { /// /// Gets whether the stream supports writing. /// - /// true if the stream supports writing; otherwise, false. + /// if the stream supports writing; otherwise, . public override bool CanWrite { get { return Stream.CanWrite; } } @@ -161,7 +129,7 @@ public override bool CanWrite { /// /// Gets whether the stream supports seeking. /// - /// true if the stream supports seeking; otherwise, false. + /// if the stream supports seeking; otherwise, . public override bool CanSeek { get { return false; } } @@ -172,7 +140,7 @@ public override bool CanSeek { /// /// Gets whether the stream supports I/O timeouts. /// - /// true if the stream supports I/O timeouts; otherwise, false. + /// if the stream supports I/O timeouts; otherwise, . public override bool CanTimeout { get { return Stream.CanTimeout; } } @@ -222,7 +190,7 @@ public override int WriteTimeout { /// public override long Position { get { return Stream.Position; } - set { Stream.Position = value; } + set { throw new NotSupportedException (); } } /// @@ -243,80 +211,74 @@ public override long Length { get { return Stream.Length; } } - void Poll (SelectMode mode, CancellationToken cancellationToken) + void AlignReadAheadBuffer (out int offset, out int count) { -#if NETFX_CORE - cancellationToken.ThrowIfCancellationRequested (); -#else - if (!cancellationToken.CanBeCanceled) - return; + int left = inputEnd - inputIndex; - if (Socket != null) { - do { - cancellationToken.ThrowIfCancellationRequested (); - // wait 1/4 second and then re-check for cancellation - } while (!Socket.Poll (250000, mode)); + if (left > 0) { + if (inputIndex > 0) { + // move all of the remaining input to the beginning of the buffer + Buffer.BlockCopy (input, inputIndex, input, 0, left); + inputEnd = left; + inputIndex = 0; + } } else { - cancellationToken.ThrowIfCancellationRequested (); + inputIndex = 0; + inputEnd = 0; } -#endif + + count = BlockSize - inputEnd; + offset = inputEnd; } - unsafe int ReadAhead (CancellationToken cancellationToken) + int ReadAhead (CancellationToken cancellationToken) { - int left = inputEnd - inputIndex; - int start = inputStart; - int end = inputEnd; - int nread; + AlignReadAheadBuffer (out int offset, out int count); - if (left > 0) { - int index = inputIndex; - - // attempt to align the end of the remaining input with ReadAheadSize - if (index >= start) { - start -= Math.Min (ReadAheadSize, left); - Buffer.BlockCopy (input, index, input, start, left); - index = start; - start += left; - } else if (index > 0) { - int shift = Math.Min (index, end - start); - Buffer.BlockCopy (input, index, input, index - shift, left); - index -= shift; - start = index + left; + try { + var network = Stream as NetworkStream; + + cancellationToken.ThrowIfCancellationRequested (); + + network?.Poll (SelectMode.SelectRead, cancellationToken); + int nread = Stream.Read (input, offset, count); + + if (nread > 0) { + logger.LogServer (input, offset, nread); + inputEnd += nread; + + // Optimization hack used by ReadResponse + input[inputEnd] = (byte) '\n'; + } else if (lastResponse is not null) { + throw new SmtpProtocolException ($"The SMTP server has unexpectedly disconnected: {lastResponse}"); } else { - // we can't shift... - start = end; + throw new SmtpProtocolException ("The SMTP server has unexpectedly disconnected."); } - - inputIndex = index; - inputEnd = start; - } else { - inputIndex = start; - inputEnd = start; + } catch { + IsConnected = false; + throw; } - end = input.Length - PadSize; - - try { -#if !NETFX_CORE - bool buffered = !(Stream is NetworkStream); -#else - bool buffered = true; -#endif + return inputEnd - inputIndex; + } - if (buffered) { - cancellationToken.ThrowIfCancellationRequested (); + async Task ReadAheadAsync (CancellationToken cancellationToken) + { + AlignReadAheadBuffer (out int offset, out int count); - nread = Stream.Read (input, start, end - start); - } else { - Poll (SelectMode.SelectRead, cancellationToken); + try { + cancellationToken.ThrowIfCancellationRequested (); - nread = Stream.Read (input, start, end - start); - } + int nread = await Stream.ReadAsync (input, offset, count, cancellationToken).ConfigureAwait (false); if (nread > 0) { - logger.LogServer (input, start, nread); + logger.LogServer (input, offset, nread); inputEnd += nread; + + // Optimization hack used by ReadResponse + input[inputEnd] = (byte) '\n'; + } else if (lastResponse is not null) { + throw new SmtpProtocolException ($"The SMTP server has unexpectedly disconnected: {lastResponse}"); } else { throw new SmtpProtocolException ("The SMTP server has unexpectedly disconnected."); } @@ -361,12 +323,12 @@ void CheckDisposed () /// The number of bytes to read. /// The cancellation token. /// - /// is null. + /// is . /// /// /// is less than zero or greater than the length of . /// -or- - /// The is not large enough to contain bytes strting + /// The is not large enough to contain bytes starting /// at the specified . /// /// @@ -389,7 +351,7 @@ public int Read (byte[] buffer, int offset, int count, CancellationToken cancell int n; if (length < count && length <= ReadAheadSize) - ReadAhead (cancellationToken); + await ReadAheadAsync (cancellationToken).ConfigureAwait (false); length = inputEnd - inputIndex; n = Math.Min (count, length); @@ -417,12 +379,12 @@ public int Read (byte[] buffer, int offset, int count, CancellationToken cancell /// The buffer offset. /// The number of bytes to read. /// - /// is null. + /// is . /// /// /// is less than zero or greater than the length of . /// -or- - /// The is not large enough to contain bytes strting + /// The is not large enough to contain bytes starting /// at the specified . /// /// @@ -436,16 +398,138 @@ public override int Read (byte[] buffer, int offset, int count) return Read (buffer, offset, count, CancellationToken.None); } - static bool TryParseInt32 (byte[] text, ref int index, int endIndex, out int value) + /// + /// Asynchronously reads a sequence of bytes from the stream and advances the position + /// within the stream by the number of bytes read. + /// + /// + /// Reads a sequence of bytes from the stream and advances the position + /// within the stream by the number of bytes read. + /// + /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many + /// bytes are not currently available, or zero (0) if the end of the stream has been reached. + /// The buffer. + /// The buffer offset. + /// The number of bytes to read. + /// The cancellation token. + /// + /// is . + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes starting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public override Task ReadAsync (byte[] buffer, int offset, int count, CancellationToken cancellationToken) { - int startIndex = index; +#if false // Note: this code will never get called as we always use ReadResponse() instead. + CheckDisposed (); - value = 0; + ValidateArguments (buffer, offset, count); - while (index < endIndex && text[index] >= (byte) '0' && text[index] <= (byte) '9') - value = (value * 10) + (text[index++] - (byte) '0'); + int length = inputEnd - inputIndex; + int n; - return index > startIndex; + if (length < count && length <= ReadAheadSize) + await ReadAheadAsync (cancellationToken).ConfigureAwait (false); + + length = inputEnd - inputIndex; + n = Math.Min (count, length); + + Buffer.BlockCopy (input, inputIndex, buffer, offset, n); + inputIndex += n; + + return n; +#else + throw new NotImplementedException (); +#endif + } + + static bool TryParseStatusCode (byte[] text, int startIndex, out int code) + { + int endIndex = startIndex + 3; + + code = 0; + + for (int index = startIndex; index < endIndex; index++) { + if (text[index] < (byte) '0' || text[index] > (byte) '9') + return false; + + int digit = text[index] - (byte) '0'; + code = (code * 10) + digit; + } + + return true; + } + + static bool IsLegalAfterStatusCode (byte c) + { + return c == (byte) '-' || c == (byte) ' ' || c == (byte) '\r' || c == (byte) '\n'; + } + + bool ReadResponse (ByteArrayBuilder builder, ref bool newLine, ref bool more, ref int code) + { + do { + int startIndex = inputIndex; + + if (newLine) { + if (inputIndex + 3 < inputEnd) { + if (!TryParseStatusCode (input, inputIndex, out int value)) + throw new SmtpProtocolException ("Unable to parse status code returned by the server."); + + inputIndex += 3; + + if (value < 100 || !IsLegalAfterStatusCode (input[inputIndex])) + throw new SmtpProtocolException ("Invalid status code returned by the server."); + + if (code == 0) { + code = value; + } else if (value != code) { + throw new SmtpProtocolException ("The status codes returned by the server did not match."); + } + + newLine = false; + + more = input[inputIndex] == (byte) '-'; + if (more || input[inputIndex] == (byte) ' ') + inputIndex++; + + startIndex = inputIndex; + } else { + // Need input. + return true; + } + } + + // Note: This depends on ReadAhead[Async] setting input[inputEnd] = '\n' + while (input[inputIndex] != (byte) '\n') + inputIndex++; + + int endIndex = inputIndex; + if (inputIndex > startIndex && input[inputIndex - 1] == (byte) '\r') + endIndex--; + + builder.Append (input, startIndex, endIndex - startIndex); + + if (inputIndex < inputEnd && input[inputIndex] == (byte) '\n') { + if (more) + builder.Append ((byte) '\n'); + newLine = true; + inputIndex++; + } + } while (more && inputIndex < inputEnd); + + return inputIndex == inputEnd; } /// @@ -472,93 +556,216 @@ public SmtpResponse ReadResponse (CancellationToken cancellationToken) { CheckDisposed (); - using (var memory = new MemoryStream ()) { + using (var builder = new ByteArrayBuilder (256)) { bool needInput = inputIndex == inputEnd; - bool complete = false; bool newLine = true; bool more = true; int code = 0; do { - if (needInput) { + if (needInput) ReadAhead (cancellationToken); - needInput = false; - } - - complete = false; - do { - int startIndex = inputIndex; + needInput = ReadResponse (builder, ref newLine, ref more, ref code); + } while (more || !newLine); - if (newLine && inputIndex < inputEnd) { - int value; + var message = builder.ToString (); - if (!TryParseInt32 (input, ref inputIndex, inputEnd, out value)) - throw new SmtpProtocolException ("Unable to parse status code returned by the server."); + lastResponse = message; - if (inputIndex == inputEnd) { - inputIndex = startIndex; - needInput = true; - break; - } + return new SmtpResponse ((SmtpStatusCode) code, message); + } + } - if (code == 0) { - code = value; - } else if (value != code) { - throw new SmtpProtocolException ("The status codes returned by the server did not match."); - } + /// + /// Asynchronously read an SMTP server response. + /// + /// + /// Reads a full command response from the SMTP server. + /// + /// The response. + /// The cancellation token. + /// + /// The stream has been disposed. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// An SMTP protocol error occurred. + /// + public async Task ReadResponseAsync (CancellationToken cancellationToken) + { + CheckDisposed (); - newLine = false; + using (var builder = new ByteArrayBuilder (256)) { + bool needInput = inputIndex == inputEnd; + bool newLine = true; + bool more = true; + int code = 0; - if (input[inputIndex] != (byte) '\r' && input[inputIndex] != (byte) '\n') - more = input[inputIndex++] == (byte) '-'; - else - more = false; + do { + if (needInput) + await ReadAheadAsync (cancellationToken).ConfigureAwait (false); - startIndex = inputIndex; - } + needInput = ReadResponse (builder, ref newLine, ref more, ref code); + } while (more || !newLine); - while (inputIndex < inputEnd && input[inputIndex] != (byte) '\r' && input[inputIndex] != (byte) '\n') - inputIndex++; + var message = builder.ToString (); - memory.Write (input, startIndex, inputIndex - startIndex); + lastResponse = message; - if (inputIndex < inputEnd && input[inputIndex] == (byte) '\r') - inputIndex++; + return new SmtpResponse ((SmtpStatusCode) code, message); + } + } - if (inputIndex < inputEnd && input[inputIndex] == (byte) '\n') { - if (more) - memory.WriteByte (input[inputIndex]); - complete = true; - newLine = true; - inputIndex++; - } - } while (more && inputIndex < inputEnd); + unsafe bool TryQueueCommand (Encoder encoder, string command, ref int index) + { + fixed (char* cmd = command) { + int outputLeft = output.Length - outputIndex; + int charCount = command.Length - index; + char* chars = cmd + index; + + var needed = encoder.GetByteCount (chars, charCount, true); + + if (needed > output.Length) { + // If the command we are trying to queue is larger than the output buffer and we + // already have some commands queued in the output buffer, then flush the queue + // before queuing this command. + if (outputIndex > 0) + return false; + } else if (needed > outputLeft && index == 0) { + // If we are trying to queue a new command (index == 0) and we need more space than + // what remains in the output buffer, then flush the output buffer before queueing + // the new command. Some servers do not handle receiving partial commands well. + return false; + } - if (inputIndex == inputEnd) - needInput = true; - } while (more || !complete); + fixed (byte* outbuf = output) { + byte* outptr = outbuf + outputIndex; - string message = null; + encoder.Convert (chars, charCount, outptr, outputLeft, true, out int charsUsed, out int bytesUsed, out bool completed); + outputIndex += bytesUsed; + index += charsUsed; - try { -#if !NETFX_CORE && !NETSTANDARD - message = UTF8.GetString (memory.GetBuffer (), 0, (int) memory.Length); -#else - message = UTF8.GetString (memory.ToArray (), 0, (int) memory.Length); -#endif - } catch (DecoderFallbackException) { -#if !NETFX_CORE && !NETSTANDARD - message = Latin1.GetString (memory.GetBuffer (), 0, (int) memory.Length); -#else - message = Latin1.GetString (memory.ToArray (), 0, (int) memory.Length); -#endif + return completed; } - - return new SmtpResponse ((SmtpStatusCode) code, message); } } + /// + /// Queue a command to the SMTP server. + /// + /// + /// Queues a command to the SMTP server. + /// + /// The command. + /// The cancellation token. + /// + /// The stream has been disposed. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public void QueueCommand (string command, CancellationToken cancellationToken) + { + var encoder = Encoding.UTF8.GetEncoder (); + int index = 0; + + while (!TryQueueCommand (encoder, command, ref index)) + Flush (cancellationToken); + } + + /// + /// Asynchronously queue a command to the SMTP server. + /// + /// + /// Asynchronously queues a command to the SMTP server. + /// + /// The command. + /// The cancellation token. + /// + /// The stream has been disposed. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public async Task QueueCommandAsync (string command, CancellationToken cancellationToken) + { + var encoder = Encoding.UTF8.GetEncoder (); + int index = 0; + + while (!TryQueueCommand (encoder, command, ref index)) + await FlushAsync (cancellationToken).ConfigureAwait (false); + } + + /// + /// Send a command to the SMTP server. + /// + /// + /// Sends a command to the SMTP server and reads the response. + /// + /// The response. + /// The command. + /// The cancellation token. + /// + /// The stream has been disposed. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// An SMTP protocol error occurred. + /// + public SmtpResponse SendCommand (string command, CancellationToken cancellationToken) + { + QueueCommand (command, cancellationToken); + Flush (cancellationToken); + + return ReadResponse (cancellationToken); + } + + /// + /// Asynchronously send a command to the SMTP server. + /// + /// + /// Asynchronously sends a command to the SMTP server and reads the response. + /// + /// The response. + /// The command. + /// The cancellation token. + /// + /// The stream has been disposed. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// An SMTP protocol error occurred. + /// + public async Task SendCommandAsync (string command, CancellationToken cancellationToken) + { + await QueueCommandAsync (command, cancellationToken).ConfigureAwait (false); + await FlushAsync (cancellationToken).ConfigureAwait (false); + + return await ReadResponseAsync (cancellationToken).ConfigureAwait (false); + } + /// /// Writes a sequence of bytes to the stream and advances the current /// position within this stream by the number of bytes written. @@ -572,12 +779,12 @@ public SmtpResponse ReadResponse (CancellationToken cancellationToken) /// The number of bytes to write. /// The cancellation token. /// - /// is null. + /// is . /// /// /// is less than zero or greater than the length of . /// -or- - /// The is not large enough to contain bytes strting + /// The is not large enough to contain bytes starting /// at the specified . /// /// @@ -599,6 +806,7 @@ public void Write (byte[] buffer, int offset, int count, CancellationToken cance ValidateArguments (buffer, offset, count); try { + var network = NetworkStream.Get (Stream); int index = offset; int left = count; @@ -615,7 +823,7 @@ public void Write (byte[] buffer, int offset, int count, CancellationToken cance if (outputIndex == BlockSize) { // flush the output buffer - Poll (SelectMode.SelectWrite, cancellationToken); + network?.Poll (SelectMode.SelectWrite, cancellationToken); Stream.Write (output, 0, BlockSize); logger.LogClient (output, 0, BlockSize); outputIndex = 0; @@ -624,7 +832,7 @@ public void Write (byte[] buffer, int offset, int count, CancellationToken cance if (outputIndex == 0) { // write blocks of data to the stream without buffering while (left >= BlockSize) { - Poll (SelectMode.SelectWrite, cancellationToken); + network?.Poll (SelectMode.SelectWrite, cancellationToken); Stream.Write (buffer, index, BlockSize); logger.LogClient (buffer, index, BlockSize); index += BlockSize; @@ -632,8 +840,10 @@ public void Write (byte[] buffer, int offset, int count, CancellationToken cance } } } - } catch { + } catch (Exception ex) { IsConnected = false; + if (ex is not OperationCanceledException) + cancellationToken.ThrowIfCancellationRequested (); throw; } } @@ -650,12 +860,12 @@ public void Write (byte[] buffer, int offset, int count, CancellationToken cance /// The offset of the first byte to write. /// The number of bytes to write. /// - /// is null. + /// is . /// /// /// is less than zero or greater than the length of . /// -or- - /// The is not large enough to contain bytes strting + /// The is not large enough to contain bytes starting /// at the specified . /// /// @@ -672,6 +882,86 @@ public override void Write (byte[] buffer, int offset, int count) Write (buffer, offset, count, CancellationToken.None); } + /// + /// Asynchronously writes a sequence of bytes to the stream and advances the current + /// position within this stream by the number of bytes written. + /// + /// + /// Writes a sequence of bytes to the stream and advances the current + /// position within this stream by the number of bytes written. + /// + /// A task that represents the asynchronous write operation. + /// The buffer to write. + /// The offset of the first byte to write. + /// The number of bytes to write. + /// The cancellation token. + /// + /// is . + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes starting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support writing. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public override async Task WriteAsync (byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + CheckDisposed (); + + ValidateArguments (buffer, offset, count); + + try { + int index = offset; + int left = count; + + while (left > 0) { + int n = Math.Min (BlockSize - outputIndex, left); + + if (outputIndex > 0 || n < BlockSize) { + // append the data to the output buffer + Buffer.BlockCopy (buffer, index, output, outputIndex, n); + outputIndex += n; + index += n; + left -= n; + } + + if (outputIndex == BlockSize) { + // flush the output buffer + await Stream.WriteAsync (output, 0, BlockSize, cancellationToken).ConfigureAwait (false); + logger.LogClient (output, 0, BlockSize); + outputIndex = 0; + } + + if (outputIndex == 0) { + // write blocks of data to the stream without buffering + while (left >= BlockSize) { + await Stream.WriteAsync (buffer, index, BlockSize, cancellationToken).ConfigureAwait (false); + logger.LogClient (buffer, index, BlockSize); + index += BlockSize; + left -= BlockSize; + } + } + } + } catch (Exception ex) { + IsConnected = false; + if (ex is not OperationCanceledException) + cancellationToken.ThrowIfCancellationRequested (); + throw; + } + } + /// /// Clears all buffers for this stream and causes any buffered data to be written /// to the underlying device. @@ -701,13 +991,18 @@ public void Flush (CancellationToken cancellationToken) return; try { - Poll (SelectMode.SelectWrite, cancellationToken); + var network = NetworkStream.Get (Stream); + + network?.Poll (SelectMode.SelectWrite, cancellationToken); Stream.Write (output, 0, outputIndex); Stream.Flush (); + logger.LogClient (output, 0, outputIndex); outputIndex = 0; - } catch { + } catch (Exception ex) { IsConnected = false; + if (ex is not OperationCanceledException) + cancellationToken.ThrowIfCancellationRequested (); throw; } } @@ -734,6 +1029,48 @@ public override void Flush () Flush (CancellationToken.None); } + /// + /// Asynchronously clears all buffers for this stream and causes any buffered data to be written + /// to the underlying device. + /// + /// + /// Clears all buffers for this stream and causes any buffered data to be written + /// to the underlying device. + /// + /// A task that represents the asynchronous flush operation. + /// The cancellation token. + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support writing. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public override async Task FlushAsync (CancellationToken cancellationToken) + { + CheckDisposed (); + + if (outputIndex == 0) + return; + + try { + await Stream.WriteAsync (output, 0, outputIndex, cancellationToken).ConfigureAwait (false); + await Stream.FlushAsync (cancellationToken).ConfigureAwait (false); + logger.LogClient (output, 0, outputIndex); + outputIndex = 0; + } catch (Exception ex) { + IsConnected = false; + if (ex is not OperationCanceledException) + cancellationToken.ThrowIfCancellationRequested (); + throw; + } + } + /// /// Sets the position within the current stream. /// @@ -768,8 +1105,8 @@ public override void SetLength (long value) /// Releases the unmanaged resources used by the and /// optionally releases the managed resources. /// - /// true to release both managed and unmanaged resources; - /// false to release only the unmanaged resources. + /// to release both managed and unmanaged resources; + /// to release only the unmanaged resources. protected override void Dispose (bool disposing) { if (disposing && !disposed) { diff --git a/MailKit/Net/SocketMetrics.cs b/MailKit/Net/SocketMetrics.cs new file mode 100644 index 0000000000..40436f49da --- /dev/null +++ b/MailKit/Net/SocketMetrics.cs @@ -0,0 +1,149 @@ +// +// SocketMetrics.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +#if NET6_0_OR_GREATER + +using System; +using System.Net; +using System.Net.Sockets; +using System.Diagnostics; +using System.Diagnostics.Metrics; +using System.Diagnostics.CodeAnalysis; + +namespace MailKit.Net { + sealed class SocketMetrics + { + readonly Counter connectCounter; + readonly Histogram connectDuration; + + public SocketMetrics (Meter meter) + { + connectCounter = meter.CreateCounter ( + name: $"{Telemetry.Socket.MeterName}.connect.count", + unit: "{attempt}", + description: "The number of times a socket attempted to connect to a remote host."); + + connectDuration = meter.CreateHistogram ( + name: $"{Telemetry.Socket.MeterName}.connect.duration", + unit: "ms", + description: "The number of milliseconds taken for a socket to connect to a remote host."); + } + + static SocketException? GetSocketException (Exception exception) + { + Exception? ex = exception; + + do { + if (ex is SocketException se) + return se; + + ex = ex.InnerException; + } while (ex is not null); + + return null; + } + + internal static bool TryGetErrorType (Exception exception, bool exceptionTypeFallback, [NotNullWhen (true)] out string? errorType) + { + if (exception is OperationCanceledException) { + errorType = "cancelled"; + return true; + } + + var socketException = GetSocketException (exception); + + if (socketException is not null) { + switch (socketException.SocketErrorCode) { + case SocketError.HostNotFound: errorType = "host_not_found"; return true; + case SocketError.HostUnreachable: errorType = "host_unreachable"; return true; + case SocketError.NetworkUnreachable: errorType = "network_unreachable"; return true; + + case SocketError.ConnectionAborted: errorType = "connection_aborted"; return true; + case SocketError.ConnectionRefused: errorType = "connection_refused"; return true; + case SocketError.ConnectionReset: errorType = "connection_reset"; return true; + + case SocketError.TimedOut: errorType = "timed_out"; return true; + case SocketError.TooManyOpenSockets: errorType = "too_many_open_sockets"; return true; + } + } + + if (exceptionTypeFallback) + errorType = exception.GetType ().FullName; + else + errorType = null; + + return errorType != null; + } + + static TagList GetTags (IPAddress ip, string host, int port, Exception? ex = null) + { + var tags = new TagList { + { "network.peer.address", ip.ToString () }, + { "server.address", host }, + { "server.port", port }, + }; + + if (ex is not null && TryGetErrorType (ex, true, out var errorType)) + tags.Add ("error.type", errorType); + + return tags; + } + + public void RecordConnected (long connectStartedTimestamp, IPAddress ip, string host, int port) + { + if (connectCounter.Enabled || connectDuration.Enabled) { + var tags = GetTags (ip, host, port); + + if (connectDuration.Enabled) { + var duration = TimeSpan.FromTicks (Stopwatch.GetTimestamp () - connectStartedTimestamp).TotalMilliseconds; + + connectDuration.Record (duration, tags); + } + + if (connectCounter.Enabled) + connectCounter.Add (1, tags); + } + } + + public void RecordConnectFailed (long connectStartedTimestamp, IPAddress ip, string host, int port, bool cancelled, Exception? ex = null) + { + if (connectCounter.Enabled || connectDuration.Enabled) { + var tags = GetTags (ip, host, port, ex); + + if (connectDuration.Enabled) { + var duration = TimeSpan.FromTicks (Stopwatch.GetTimestamp () - connectStartedTimestamp).TotalMilliseconds; + + connectDuration.Record (duration, tags); + } + + if (connectCounter.Enabled) + connectCounter.Add (1, tags); + } + } + } +} + +#endif // NET6_0_OR_GREATER diff --git a/MailKit/Net/SocketUtils.cs b/MailKit/Net/SocketUtils.cs new file mode 100644 index 0000000000..f225b320a7 --- /dev/null +++ b/MailKit/Net/SocketUtils.cs @@ -0,0 +1,259 @@ +// +// SocketUtils.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Net; +using System.Threading; +using System.Net.Sockets; +using System.Diagnostics; +using System.Threading.Tasks; + +namespace MailKit.Net +{ + static class SocketUtils + { + class SocketConnectState + { + readonly TaskCompletionSource tcs = new TaskCompletionSource (); +#if NET6_0_OR_GREATER + readonly long connectStartTicks = Stopwatch.GetTimestamp (); +#endif + readonly Socket socket; + readonly IPAddress ip; + readonly string host; + readonly int port; + + public SocketConnectState (Socket socket, IPAddress ip, string host, int port) + { + this.socket = socket; + this.ip = ip; + this.host = host; + this.port = port; + } + + public Task Task { get { return tcs.Task; } } + + public void OnCanceled () + { + tcs.TrySetCanceled (); + } + + public void OnEndConnect (IAsyncResult ar) + { + try { + socket.EndConnect (ar); + } catch (Exception ex) { + // The connection failed. Try setting an exception in case the connection hasn't also been cancelled. +#if NET6_0_OR_GREATER + bool cancelled = !tcs.TrySetException (ex); + + Telemetry.Socket.Metrics?.RecordConnectFailed (connectStartTicks, ip, host, port, cancelled, ex); +#else + tcs.TrySetException (ex); +#endif + socket.Dispose (); + return; + } + + // The connection was successful. + if (tcs.TrySetResult (true)) { +#if NET6_0_OR_GREATER + Telemetry.Socket.Metrics?.RecordConnected (connectStartTicks, ip, host, port); +#endif + return; + } + + // Note: If we get this far, then it means that the connection has been cancelled. +#if NET6_0_OR_GREATER + Telemetry.Socket.Metrics?.RecordConnectFailed (connectStartTicks, ip, host, port, true); +#endif + + try { + socket.Disconnect (false); + socket.Dispose (); + } catch { + return; + } + } + } + + static void OnEndConnect (IAsyncResult ar) + { + var state = (SocketConnectState) ar.AsyncState!; + + state.OnEndConnect (ar); + } + + public static Socket Connect (string host, int port, IPEndPoint? localEndPoint, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested (); + + var ipAddresses = Dns.GetHostAddresses (host); + + for (int i = 0; i < ipAddresses.Length; i++) { + cancellationToken.ThrowIfCancellationRequested (); + + var socket = new Socket (ipAddresses[i].AddressFamily, SocketType.Stream, ProtocolType.Tcp); + + try { + if (localEndPoint != null) + socket.Bind (localEndPoint); + } catch { + socket.Dispose (); + + if (i + 1 == ipAddresses.Length) + throw; + + continue; + } + +#if NET6_0_OR_GREATER + long connectStartTicks = Stopwatch.GetTimestamp (); +#endif + + try { + if (cancellationToken.CanBeCanceled) { + var state = new SocketConnectState (socket, ipAddresses[i], host, port); + + using (var registration = cancellationToken.Register (state.OnCanceled, false)) { + var ar = socket.BeginConnect (ipAddresses[i], port, OnEndConnect, state); + state.Task.GetAwaiter ().GetResult (); + } + } else { + socket.Connect (ipAddresses[i], port); + +#if NET6_0_OR_GREATER + Telemetry.Socket.Metrics?.RecordConnected (connectStartTicks, ipAddresses[i], host, port); +#endif + } + + return socket; + } catch (OperationCanceledException) { + throw; + } catch (Exception ex) { + if (!cancellationToken.CanBeCanceled) { +#if NET6_0_OR_GREATER + Telemetry.Socket.Metrics?.RecordConnectFailed (connectStartTicks, ipAddresses[i], host, port, false, ex); +#endif + + socket.Dispose (); + } + + if (i + 1 == ipAddresses.Length) + throw; + } + } + + throw new IOException (string.Format ("Failed to resolve host: {0}", host)); + } + + public static async Task ConnectAsync (string host, int port, IPEndPoint? localEndPoint, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested (); + +#if NET6_0_OR_GREATER + var ipAddresses = await Dns.GetHostAddressesAsync (host, cancellationToken).ConfigureAwait (false); +#else + var ipAddresses = await Dns.GetHostAddressesAsync (host).ConfigureAwait (false); +#endif + + for (int i = 0; i < ipAddresses.Length; i++) { + cancellationToken.ThrowIfCancellationRequested (); + + var socket = new Socket (ipAddresses[i].AddressFamily, SocketType.Stream, ProtocolType.Tcp); + + try { + if (localEndPoint != null) + socket.Bind (localEndPoint); + } catch { + socket.Dispose (); + + if (i + 1 == ipAddresses.Length) + throw; + + continue; + } + + try { + var state = new SocketConnectState (socket, ipAddresses[i], host, port); + + using (var registration = cancellationToken.Register (state.OnCanceled, false)) { + var ar = socket.BeginConnect (ipAddresses[i], port, OnEndConnect, state); + await state.Task.ConfigureAwait (false); + } + + return socket; + } catch (OperationCanceledException) { + throw; + } catch { + if (i + 1 == ipAddresses.Length) + throw; + } + } + + throw new IOException (string.Format ("Failed to resolve host: {0}", host)); + } + + public static Socket Connect (string host, int port, IPEndPoint? localEndPoint, int timeout, CancellationToken cancellationToken) + { + using (var ts = new CancellationTokenSource (timeout)) { + using (var linked = CancellationTokenSource.CreateLinkedTokenSource (cancellationToken, ts.Token)) { + try { + return Connect (host, port, localEndPoint, linked.Token); + } catch (OperationCanceledException) { + if (!cancellationToken.IsCancellationRequested) + throw new TimeoutException (); + throw; + } + } + } + } + + public static async Task ConnectAsync (string host, int port, IPEndPoint? localEndPoint, int timeout, CancellationToken cancellationToken) + { + using (var ts = new CancellationTokenSource (timeout)) { + using (var linked = CancellationTokenSource.CreateLinkedTokenSource (cancellationToken, ts.Token)) { + try { + return await ConnectAsync (host, port, localEndPoint, linked.Token).ConfigureAwait (false); + } catch (OperationCanceledException) { + if (!cancellationToken.IsCancellationRequested) + throw new TimeoutException (); + throw; + } + } + } + } + + public static void Poll (Socket socket, SelectMode mode, CancellationToken cancellationToken) + { + do { + cancellationToken.ThrowIfCancellationRequested (); + // wait 1/4 second and then re-check for cancellation + } while (!socket.Poll (250000, mode)); + } + } +} diff --git a/MailKit/NullProtocolLogger.cs b/MailKit/NullProtocolLogger.cs index 2e60dfd823..39b98e137a 100644 --- a/MailKit/NullProtocolLogger.cs +++ b/MailKit/NullProtocolLogger.cs @@ -1,9 +1,9 @@ -// +// // NullProtocolLogger.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -50,6 +50,15 @@ public NullProtocolLogger () #region IProtocolLogger implementation + /// + /// Get or set the authentication secret detector. + /// + /// + /// Gets or sets the authentication secret detector. + /// + /// The authentication secret detector. + public IAuthenticationSecretDetector? AuthenticationSecretDetector { get; set; } + /// /// Logs a connection to the specified URI. /// @@ -71,7 +80,7 @@ public void LogConnect (Uri uri) /// The offset of the first byte to log. /// The number of bytes to log. /// - /// is null. + /// is . /// public void LogClient (byte[] buffer, int offset, int count) { @@ -87,7 +96,7 @@ public void LogClient (byte[] buffer, int offset, int count) /// The offset of the first byte to log. /// The number of bytes to log. /// - /// is null. + /// is . /// public void LogServer (byte[] buffer, int offset, int count) { diff --git a/MailKit/PreviewOptions.cs b/MailKit/PreviewOptions.cs new file mode 100644 index 0000000000..b68f5162ad --- /dev/null +++ b/MailKit/PreviewOptions.cs @@ -0,0 +1,46 @@ +// +// PreviewOptions.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +namespace MailKit { +#if ENABLE_LAZY_PREVIEW_API + + /// + /// A set of options for fetching the preview text for messages. + /// + public enum PreviewOptions + { + /// + /// No options specified. + /// + None, + + /// + /// The preview text should only be fetched if the server has it instantly available (cached). + /// + Lazy + } +#endif +} diff --git a/MailKit/ProgressStream.cs b/MailKit/ProgressStream.cs index 73934ddbd7..e2b65d027e 100644 --- a/MailKit/ProgressStream.cs +++ b/MailKit/ProgressStream.cs @@ -1,9 +1,9 @@ -// +// // ProgressStream.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -27,19 +27,23 @@ using System; using System.IO; using System.Threading; +using System.Threading.Tasks; using MimeKit.IO; namespace MailKit { class ProgressStream : Stream, ICancellableStream { - readonly ICancellableStream cancellable; + readonly ICancellableStream? cancellable; public ProgressStream (Stream source, Action update) { if (source == null) throw new ArgumentNullException (nameof (source)); + if (update == null) + throw new ArgumentNullException (nameof (update)); + cancellable = source as ICancellableStream; Source = source; Update = update; @@ -75,7 +79,7 @@ public override long Length { public override long Position { get { return Source.Position; } - set { Source.Position = value; } + set { Seek (value, SeekOrigin.Begin); } } public override int ReadTimeout { @@ -113,6 +117,16 @@ public override int Read (byte[] buffer, int offset, int count) return n; } + public override async Task ReadAsync (byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + int n; + + if ((n = await Source.ReadAsync (buffer, offset, count, cancellationToken).ConfigureAwait (false)) > 0) + Update (n); + + return n; + } + public void Write (byte[] buffer, int offset, int count, CancellationToken cancellationToken) { if (cancellable != null) @@ -132,6 +146,14 @@ public override void Write (byte[] buffer, int offset, int count) Update (count); } + public override async Task WriteAsync (byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + await Source.WriteAsync (buffer, offset, count, cancellationToken).ConfigureAwait (false); + + if (count > 0) + Update (count); + } + public override long Seek (long offset, SeekOrigin origin) { throw new NotSupportedException ("The stream does not support seeking."); @@ -150,6 +172,11 @@ public override void Flush () Source.Flush (); } + public override Task FlushAsync (CancellationToken cancellationToken) + { + return Source.FlushAsync (cancellationToken); + } + public override void SetLength (long value) { throw new NotSupportedException ("The stream does not support resizing."); diff --git a/MailKit/Properties/AssemblyInfo.cs b/MailKit/Properties/AssemblyInfo.cs index 936dc17443..342cacfd03 100644 --- a/MailKit/Properties/AssemblyInfo.cs +++ b/MailKit/Properties/AssemblyInfo.cs @@ -3,7 +3,7 @@ // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -34,10 +34,10 @@ [assembly: AssemblyTitle ("MailKit")] [assembly: AssemblyDescription ("A cross-platform mail client library.")] [assembly: AssemblyConfiguration ("")] -[assembly: AssemblyCompany ("Xamarin Inc.")] +[assembly: AssemblyCompany (".NET Foundation")] [assembly: AssemblyProduct ("MailKit")] -[assembly: AssemblyCopyright ("Copyright © 2013-2017 Xamarin Inc. (www.xamarin.com)")] -[assembly: AssemblyTrademark ("Xamarin Inc.")] +[assembly: AssemblyCopyright ("Copyright © 2013-2026 .NET Foundation and Contributors")] +[assembly: AssemblyTrademark (".NET Foundation")] [assembly: AssemblyCulture ("")] // Setting ComVisible to false makes the types in this assembly not visible @@ -79,6 +79,6 @@ // // If there have only been bug fixes, bump the Micro Version and/or the Build Number // in the AssemblyFileVersion attribute. -[assembly: AssemblyInformationalVersion ("1.16.1.0")] -[assembly: AssemblyFileVersion ("1.16.1.0")] -[assembly: AssemblyVersion ("1.16.0.0")] +[assembly: AssemblyInformationalVersion ("4.17.0.0")] +[assembly: AssemblyFileVersion ("4.17.0.0")] +[assembly: AssemblyVersion ("4.17.0.0")] diff --git a/MailKit/ProtocolException.cs b/MailKit/ProtocolException.cs index 4ec8829824..bc2f7dafd9 100644 --- a/MailKit/ProtocolException.cs +++ b/MailKit/ProtocolException.cs @@ -1,9 +1,9 @@ -// +// // ProtocolException.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -46,6 +46,8 @@ namespace MailKit { #endif public abstract class ProtocolException : Exception { + const string ProtocolLogHelpLink = "https://github.com/jstedfast/MailKit/blob/master/FAQ.md#protocol-log"; + #if SERIALIZABLE /// /// Initializes a new instance of the class. @@ -56,6 +58,7 @@ public abstract class ProtocolException : Exception /// The serialization info. /// The streaming context. [SecuritySafeCritical] + [Obsolete ("This API supports obsolete formatter-based serialization. It should not be called or extended by application code.")] protected ProtocolException (SerializationInfo info, StreamingContext context) : base (info, context) { } @@ -71,7 +74,7 @@ protected ProtocolException (SerializationInfo info, StreamingContext context) : /// An inner exception. protected ProtocolException (string message, Exception innerException) : base (message, innerException) { - HelpLink = "https://github.com/jstedfast/MailKit/blob/master/FAQ.md#ProtocolLog"; + HelpLink = ProtocolLogHelpLink; } /// @@ -83,7 +86,7 @@ protected ProtocolException (string message, Exception innerException) : base (m /// The error message. protected ProtocolException (string message) : base (message) { - HelpLink = "https://github.com/jstedfast/MailKit/blob/master/FAQ.md#ProtocolLog"; + HelpLink = ProtocolLogHelpLink; } /// @@ -94,7 +97,7 @@ protected ProtocolException (string message) : base (message) /// protected ProtocolException () { - HelpLink = "https://github.com/jstedfast/MailKit/blob/master/FAQ.md#ProtocolLog"; + HelpLink = ProtocolLogHelpLink; } } } diff --git a/MailKit/ProtocolLogger.cs b/MailKit/ProtocolLogger.cs index 130ce9ab41..b282cf975c 100644 --- a/MailKit/ProtocolLogger.cs +++ b/MailKit/ProtocolLogger.cs @@ -1,9 +1,9 @@ -// +// // ProtocolLogger.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -27,44 +27,51 @@ using System; using System.IO; using System.Text; - -#if NETFX_CORE -using Encoding = Portable.Text.Encoding; -#endif +using System.Globalization; namespace MailKit { /// - /// A protocol logger. + /// A default protocol logger for logging the communication between a client and server. /// /// - /// A protocol logger. + /// A default protocol logger for logging the communication between a client and server. /// /// - /// + /// /// public class ProtocolLogger : IProtocolLogger { - static readonly byte[] ClientPrefix = Encoding.ASCII.GetBytes ("C: "); - static readonly byte[] ServerPrefix = Encoding.ASCII.GetBytes ("S: "); + static byte[] defaultClientPrefix = Encoding.ASCII.GetBytes ("C: "); + static byte[] defaultServerPrefix = Encoding.ASCII.GetBytes ("S: "); + static readonly byte[] Secret = Encoding.ASCII.GetBytes ("********"); + static readonly byte[] Space = new byte[] { (byte) ' ' }; + + const string DefaultTimestampFormat = "yyyy-MM-ddTHH:mm:ssZ"; + byte[] clientPrefix = defaultClientPrefix; + byte[] serverPrefix = defaultServerPrefix; readonly Stream stream; + readonly bool leaveOpen; bool clientMidline; bool serverMidline; - bool leaveOpen; -#if !NETFX_CORE /// /// Initializes a new instance of the class. /// /// - /// Creates a new to log to a specified file. + /// Creates a new to log to a specified file. The file is created if it does not exist. /// + /// + /// + /// /// The file name. - public ProtocolLogger (string fileName) + /// if the file should be appended to; otherwise, . Defaults to . + public ProtocolLogger (string fileName, bool append = true) { - stream = File.OpenWrite (fileName); + stream = File.Open (fileName, append ? FileMode.Append : FileMode.Create, FileAccess.Write, FileShare.Read); + TimestampFormat = DefaultTimestampFormat; + RedactSecrets = true; } -#endif /// /// Initializes a new instance of the class. @@ -73,12 +80,15 @@ public ProtocolLogger (string fileName) /// Creates a new to log to a specified stream. /// /// The stream. - /// true if the stream should be left open after the protocol logger is disposed. + /// if the stream should be left open after the protocol logger is disposed. public ProtocolLogger (Stream stream, bool leaveOpen = false) { if (stream == null) throw new ArgumentNullException (nameof (stream)); + TimestampFormat = DefaultTimestampFormat; + RedactSecrets = true; + this.leaveOpen = leaveOpen; this.stream = stream; } @@ -96,8 +106,113 @@ public ProtocolLogger (Stream stream, bool leaveOpen = false) Dispose (false); } + /// + /// Get the log stream. + /// + /// + /// Gets the log stream. + /// + /// The log sstream. + public Stream Stream { + get { return stream; } + } + + /// + /// Get or set the default client prefix to use when creating new instances. + /// + /// + /// Get or set the default client prefix to use when creating new instances. + /// + /// The default client prefix. + public static string DefaultClientPrefix + { + get { return Encoding.UTF8.GetString (defaultClientPrefix); } + set { defaultClientPrefix = Encoding.UTF8.GetBytes (value); } + } + + /// + /// Get or set the default server prefix to use when creating new instances. + /// + /// + /// Get or set the default server prefix to use when creating new instances. + /// + /// The default server prefix. + public static string DefaultServerPrefix + { + get { return Encoding.UTF8.GetString (defaultServerPrefix); } + set { defaultServerPrefix = Encoding.UTF8.GetBytes (value); } + } + + /// + /// Get or set the client prefix to use when logging client messages. + /// + /// + /// Gets or sets the client prefix to use when logging client messages. + /// + /// The client prefix. + public string ClientPrefix + { + get { return Encoding.UTF8.GetString (clientPrefix); } + set { clientPrefix = Encoding.UTF8.GetBytes (value); } + } + + /// + /// Get or set the server prefix to use when logging server messages. + /// + /// + /// Gets or sets the server prefix to use when logging server messages. + /// + /// The server prefix. + public string ServerPrefix + { + get { return Encoding.UTF8.GetString (serverPrefix); } + set { serverPrefix = Encoding.UTF8.GetBytes (value); } + } + + /// + /// Get or set whether or not authentication secrets should be redacted. + /// + /// + /// Gets or sets whether or not authentication secrets should be redacted. + /// + /// if authentication secrets should be redacted; otherwise, . + public bool RedactSecrets { + get; set; + } + + /// + /// Get or set whether timestamps should be logged. + /// + /// + /// Gets or sets whether or not timestamps should be logged. + /// + /// if timestamps should be logged; otherwise, . + public bool LogTimestamps { + get; set; + } + + /// + /// Get or set the date and time serialization format that should be used when logging timestamps. + /// + /// + /// Gets or sets the date and time serialization format that should be used when logging timestamps. + /// + /// The date and time serialization format that should be used when logging timestamps. + public string TimestampFormat { + get; set; + } + #region IProtocolLogger implementation + /// + /// Get or set the authentication secret detector. + /// + /// + /// Gets or sets the authentication secret detector. + /// + /// The authentication secret detector. + public IAuthenticationSecretDetector? AuthenticationSecretDetector { get; set; } + static void ValidateArguments (byte[] buffer, int offset, int count) { if (buffer == null) @@ -110,7 +225,7 @@ static void ValidateArguments (byte[] buffer, int offset, int count) throw new ArgumentOutOfRangeException (nameof (count)); } - void Log (byte[] prefix, ref bool midline, byte[] buffer, int offset, int count) + void Log (byte[] prefix, ref bool midline, byte[] buffer, int offset, int count, bool isClient) { int endIndex = offset + count; int index = offset; @@ -122,8 +237,15 @@ void Log (byte[] prefix, ref bool midline, byte[] buffer, int offset, int count) while (index < endIndex && buffer[index] != (byte) '\n') index++; - if (!midline) + if (!midline) { + if (LogTimestamps) { + var timestamp = Encoding.ASCII.GetBytes (DateTime.UtcNow.ToString (TimestampFormat, CultureInfo.InvariantCulture)); + stream.Write (timestamp, 0, timestamp.Length); + stream.Write (Space, 0, Space.Length); + } + stream.Write (prefix, 0, prefix.Length); + } if (index < endIndex && buffer[index] == (byte) '\n') { midline = false; @@ -132,6 +254,17 @@ void Log (byte[] prefix, ref bool midline, byte[] buffer, int offset, int count) midline = true; } + if (isClient && RedactSecrets && AuthenticationSecretDetector != null) { + var secrets = AuthenticationSecretDetector.DetectSecrets (buffer, start, index - start); + + foreach (var secret in secrets) { + if (secret.StartIndex > start) + stream.Write (buffer, start, secret.StartIndex - start); + start = secret.StartIndex + secret.Length; + stream.Write (Secret, 0, Secret.Length); + } + } + stream.Write (buffer, start, index - start); } @@ -146,7 +279,7 @@ void Log (byte[] prefix, ref bool midline, byte[] buffer, int offset, int count) /// /// The URI. /// - /// is null. + /// is . /// /// /// The logger has been disposed. @@ -159,7 +292,14 @@ public void LogConnect (Uri uri) if (uri == null) throw new ArgumentNullException (nameof (uri)); - var message = string.Format ("Connected to {0}\r\n", uri); + string message; + + if (LogTimestamps) { + message = string.Format ("{0} Connected to {1}\r\n", DateTime.UtcNow.ToString (TimestampFormat, CultureInfo.InvariantCulture), uri); + } else { + message = string.Format ("Connected to {0}\r\n", uri); + } + var buf = Encoding.ASCII.GetBytes (message); if (clientMidline || serverMidline) { @@ -177,18 +317,21 @@ public void LogConnect (Uri uri) /// Logs a sequence of bytes sent by the client. /// /// - /// Logs a sequence of bytes sent by the client. + /// Logs a sequence of bytes sent by the client. + /// is called by the upon every successful + /// write operation to its underlying network stream, passing the exact same , + /// , and arguments to the logging function. /// /// The buffer to log. /// The offset of the first byte to log. /// The number of bytes to log. /// - /// is null. + /// is . /// /// /// is less than zero or greater than the length of . /// -or- - /// The is not large enough to contain bytes strting + /// The is not large enough to contain bytes starting /// at the specified . /// /// @@ -201,25 +344,27 @@ public void LogClient (byte[] buffer, int offset, int count) { ValidateArguments (buffer, offset, count); - Log (ClientPrefix, ref clientMidline, buffer, offset, count); + Log (clientPrefix, ref clientMidline, buffer, offset, count, true); } /// /// Logs a sequence of bytes sent by the server. /// /// - /// Logs a sequence of bytes sent by the server. + /// Logs a sequence of bytes sent by the server. + /// is called by the upon every successful + /// read of its underlying network stream with the exact buffer that was read. /// /// The buffer to log. /// The offset of the first byte to log. /// The number of bytes to log. /// - /// is null. + /// is . /// /// /// is less than zero or greater than the length of . /// -or- - /// The is not large enough to contain bytes strting + /// The is not large enough to contain bytes starting /// at the specified . /// /// @@ -232,7 +377,7 @@ public void LogServer (byte[] buffer, int offset, int count) { ValidateArguments (buffer, offset, count); - Log (ServerPrefix, ref serverMidline, buffer, offset, count); + Log (serverPrefix, ref serverMidline, buffer, offset, count, false); } #endregion @@ -247,8 +392,8 @@ public void LogServer (byte[] buffer, int offset, int count) /// Releases the unmanaged resources used by the and /// optionally releases the managed resources. /// - /// true to release both managed and unmanaged resources; - /// false to release only the unmanaged resources. + /// to release both managed and unmanaged resources; + /// to release only the unmanaged resources. protected virtual void Dispose (bool disposing) { if (disposing && !leaveOpen) diff --git a/MailKit/ReplaceRequest.cs b/MailKit/ReplaceRequest.cs new file mode 100644 index 0000000000..b6b42d7e50 --- /dev/null +++ b/MailKit/ReplaceRequest.cs @@ -0,0 +1,122 @@ +// +// ReplaceRequest.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Collections.Generic; + +using MimeKit; + +namespace MailKit { + /// + /// A request for replacing a message in a folder. + /// + /// + /// A request for replacing a message in a folder. + /// + public class ReplaceRequest : AppendRequest, IReplaceRequest + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The message. + /// The message flags. + /// + /// is . + /// + public ReplaceRequest (MimeMessage message, MessageFlags flags = MessageFlags.None) : base (message, flags) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The message. + /// The message flags. + /// The message keywords. + /// + /// is . + /// -or- + /// is . + /// + public ReplaceRequest (MimeMessage message, MessageFlags flags, IEnumerable keywords) : base (message, flags, keywords) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The message. + /// The message flags. + /// The internal date of the message. + /// + /// is . + /// + public ReplaceRequest (MimeMessage message, MessageFlags flags, DateTimeOffset internalDate) : base (message, flags, internalDate) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The message. + /// The message flags. + /// The message keywords. + /// The internal date of the message. + /// + /// is . + /// -or- + /// is . + /// + public ReplaceRequest (MimeMessage message, MessageFlags flags, IEnumerable keywords, DateTimeOffset internalDate) : base (message, flags, keywords, internalDate) + { + } + + /// + /// Get or set the folder where the replacement message should be appended. + /// + /// + /// Gets or sets the folder where the replacement message should be appended. + /// If no destination folder is specified, then the replacement message will be + /// appended to the original folder. + /// + /// The destination folder. + public IMailFolder? Destination { + get; set; + } + } +} diff --git a/MailKit/Resources/Resource.designer.cs b/MailKit/Resources/Resource.designer.cs deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/MailKit/Search/AnnotationSearchQuery.cs b/MailKit/Search/AnnotationSearchQuery.cs new file mode 100644 index 0000000000..57a69469cf --- /dev/null +++ b/MailKit/Search/AnnotationSearchQuery.cs @@ -0,0 +1,105 @@ +// +// AnnotationSearchQuery.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MailKit.Search +{ + /// + /// An annotation-based search query. + /// + /// + /// An annotation-based search query. + /// + public class AnnotationSearchQuery : SearchQuery + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new annotation-based search query. + /// + /// The annotation entry. + /// The annotation attribute. + /// The annotation attribute value. + /// + /// is . + /// -or- + /// is . + /// + /// + /// is not a valid attribute for searching. + /// + public AnnotationSearchQuery (AnnotationEntry entry, AnnotationAttribute attribute, string value) : base (SearchTerm.Annotation) + { + if (entry is null) + throw new ArgumentNullException (nameof (entry)); + + if (attribute is null) + throw new ArgumentNullException (nameof (attribute)); + + if (attribute.Name != "value") + throw new ArgumentException ("Only the \"value\", \"value.priv\", and \"value.shared\" attributes can be searched.", nameof (attribute)); + + Attribute = attribute; + Entry = entry; + Value = value; + } + + /// + /// Get the annotation entry. + /// + /// + /// Gets the annotation entry. + /// + /// The annotation entry. + public AnnotationEntry Entry { + get; private set; + } + + /// + /// Get the annotation attribute. + /// + /// + /// Gets the annotation attribute. + /// + /// The annotation attribute. + public AnnotationAttribute Attribute { + get; private set; + } + + /// + /// Get the annotation attribute value. + /// + /// + /// Gets the annotation attribute value. + /// + /// The annotation attribute value. + public string Value { + get; private set; + } + } +} diff --git a/MailKit/Search/BinarySearchQuery.cs b/MailKit/Search/BinarySearchQuery.cs index c305cf5cc0..bad66b6776 100644 --- a/MailKit/Search/BinarySearchQuery.cs +++ b/MailKit/Search/BinarySearchQuery.cs @@ -1,9 +1,9 @@ -// +// // BinarySearchQuery.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -45,9 +45,9 @@ public class BinarySearchQuery : SearchQuery /// The left expression. /// The right expression. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// public BinarySearchQuery (SearchTerm term, SearchQuery left, SearchQuery right) : base (term) { diff --git a/MailKit/Search/DateSearchQuery.cs b/MailKit/Search/DateSearchQuery.cs index fb6d569cc4..42774e0c52 100644 --- a/MailKit/Search/DateSearchQuery.cs +++ b/MailKit/Search/DateSearchQuery.cs @@ -1,9 +1,9 @@ -// +// // DateSearchQuery.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal diff --git a/MailKit/Search/FilterSearchQuery.cs b/MailKit/Search/FilterSearchQuery.cs index 8e3aa29e96..f849bdf3d6 100644 --- a/MailKit/Search/FilterSearchQuery.cs +++ b/MailKit/Search/FilterSearchQuery.cs @@ -1,9 +1,9 @@ -// +// // FilterSearchQuery.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Jeffrey Stedfast +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -44,7 +44,7 @@ public class FilterSearchQuery : SearchQuery /// /// The name of the filter. /// - /// is null. + /// is . /// /// /// is empty. diff --git a/MailKit/Search/HeaderSearchQuery.cs b/MailKit/Search/HeaderSearchQuery.cs index 51ea434f0e..79f142ed2d 100644 --- a/MailKit/Search/HeaderSearchQuery.cs +++ b/MailKit/Search/HeaderSearchQuery.cs @@ -1,9 +1,9 @@ -// +// // HeaderSearchQuery.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -44,9 +44,9 @@ public class HeaderSearchQuery : SearchQuery /// The header field name. /// The value to match against. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// /// /// is empty. diff --git a/MailKit/Search/ISearchQueryOptimizer.cs b/MailKit/Search/ISearchQueryOptimizer.cs index 28217b3415..6e1064526f 100644 --- a/MailKit/Search/ISearchQueryOptimizer.cs +++ b/MailKit/Search/ISearchQueryOptimizer.cs @@ -1,9 +1,9 @@ -// +// // ISearchQueryOptimizer.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal diff --git a/MailKit/Search/NumericSearchQuery.cs b/MailKit/Search/NumericSearchQuery.cs index b49bd074f2..5e81af80e9 100644 --- a/MailKit/Search/NumericSearchQuery.cs +++ b/MailKit/Search/NumericSearchQuery.cs @@ -1,9 +1,9 @@ -// +// // NumericSearchQuery.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal diff --git a/MailKit/Search/OrderBy.cs b/MailKit/Search/OrderBy.cs index 85e8459a56..1975203a52 100644 --- a/MailKit/Search/OrderBy.cs +++ b/MailKit/Search/OrderBy.cs @@ -1,9 +1,9 @@ -// +// // OrderBy.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -32,7 +32,7 @@ namespace MailKit.Search { /// /// /// You can combine multiple rules to specify the sort - /// order that + /// order that /// should return the results in. /// public class OrderBy @@ -40,6 +40,9 @@ public class OrderBy /// /// Initializes a new instance of the class. /// + /// + /// Creates a new instance. + /// /// The field to sort by. /// The sort order. /// @@ -85,10 +88,10 @@ public SortOrder Order { public static readonly OrderBy Arrival = new OrderBy (OrderByType.Arrival, SortOrder.Ascending); /// - /// Sort results by arrival date in desending order. + /// Sort results by arrival date in descending order. /// /// - /// Sort results by arrival date in desending order. + /// Sort results by arrival date in descending order. /// public static readonly OrderBy ReverseArrival = new OrderBy (OrderByType.Arrival, SortOrder.Descending); diff --git a/MailKit/Search/OrderByAnnotation.cs b/MailKit/Search/OrderByAnnotation.cs new file mode 100644 index 0000000000..bce300cf0f --- /dev/null +++ b/MailKit/Search/OrderByAnnotation.cs @@ -0,0 +1,94 @@ +// +// OrderByAnnotation.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MailKit.Search { + /// + /// Specifies an annotation-based sort order for search results. + /// + /// + /// You can combine multiple rules to specify the sort + /// order that + /// should return the results in. + /// + public class OrderByAnnotation : OrderBy + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The annotation entry to sort by. + /// The annotation attribute to use for sorting. + /// The sort order. + /// + /// is . + /// -or- + /// is . + /// + /// + /// is not a valid attribute for sorting. + /// + public OrderByAnnotation (AnnotationEntry entry, AnnotationAttribute attribute, SortOrder order) : base (OrderByType.Annotation, order) + { + if (entry is null) + throw new ArgumentNullException (nameof (entry)); + + if (attribute is null) + throw new ArgumentNullException (nameof (attribute)); + + if (attribute.Name != "value" || attribute.Scope == AnnotationScope.Both) + throw new ArgumentException ("Only the \"value.priv\" and \"value.shared\" attributes can be used for sorting.", nameof (attribute)); + + Entry = entry; + Attribute = attribute; + } + + /// + /// Get the annotation entry. + /// + /// + /// Gets the annotation entry. + /// + /// The annotation entry. + public AnnotationEntry Entry { + get; private set; + } + + /// + /// Get the annotation attribute. + /// + /// + /// Gets the annotation attribute. + /// + /// The annotation attribute. + public AnnotationAttribute Attribute { + get; private set; + } + } +} diff --git a/MailKit/Search/OrderByType.cs b/MailKit/Search/OrderByType.cs index 4e2246cfe6..d9de49c056 100644 --- a/MailKit/Search/OrderByType.cs +++ b/MailKit/Search/OrderByType.cs @@ -1,9 +1,9 @@ -// +// // OrderByType.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -32,6 +32,11 @@ namespace MailKit.Search { /// The field to sort by. /// public enum OrderByType { + /// + /// Sort by an annotation value. + /// + Annotation, + /// /// Sort by the arrival date. /// diff --git a/MailKit/Search/SearchOptions.cs b/MailKit/Search/SearchOptions.cs index 3b098c7cae..56c1a44b08 100644 --- a/MailKit/Search/SearchOptions.cs +++ b/MailKit/Search/SearchOptions.cs @@ -1,9 +1,9 @@ -// +// // SearchOptions.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal diff --git a/MailKit/Search/SearchQuery.cs b/MailKit/Search/SearchQuery.cs index 3b36421682..69f6eee7bf 100644 --- a/MailKit/Search/SearchQuery.cs +++ b/MailKit/Search/SearchQuery.cs @@ -1,9 +1,9 @@ -// +// // SearchQuery.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -73,7 +73,8 @@ public SearchTerm Term { /// Match all messages in the folder. /// /// - /// Matches all messages in the folder. + /// Matches all messages in the folder. + /// This is equivalent to the ALL search key as defined in rfc3501. /// public static readonly SearchQuery All = new SearchQuery (SearchTerm.All); @@ -87,9 +88,9 @@ public SearchTerm Term { /// The first operand. /// The second operand. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// public static BinarySearchQuery And (SearchQuery left, SearchQuery right) { @@ -111,7 +112,7 @@ public static BinarySearchQuery And (SearchQuery left, SearchQuery right) /// A representing the conditional AND operation. /// An additional query to execute. /// - /// is null. + /// is . /// public BinarySearchQuery And (SearchQuery expr) { @@ -121,11 +122,39 @@ public BinarySearchQuery And (SearchQuery expr) return new BinarySearchQuery (SearchTerm.And, this, expr); } + /// + /// Match messages with the specified annotation. + /// + /// + /// Matches messages with the specified annotation. + /// This is equivalent to the ANNOTATION search key as defined in rfc5257 + /// and is therefor only available for use with IMAP servers that support the ANNOTATE extension. + /// + /// + /// + /// The annotation entry. + /// The annotation attribute. + /// The annotation attribute value. + /// A . + /// + /// is . + /// -or- + /// is . + /// + /// + /// is not a valid attribute for searching. + /// + public static AnnotationSearchQuery AnnotationsContain (AnnotationEntry entry, AnnotationAttribute attribute, string value) + { + return new AnnotationSearchQuery (entry, attribute, value); + } + /// /// Match messages with the flag set. /// /// - /// Matches messages with the flag set. + /// Matches messages with the flag set. + /// This is equivalent to the ANSWERED search key as defined in rfc3501. /// public static readonly SearchQuery Answered = new SearchQuery (SearchTerm.Answered); @@ -133,12 +162,13 @@ public BinarySearchQuery And (SearchQuery expr) /// Match messages where the Bcc header contains the specified text. /// /// - /// Matches messages where the Bcc header contains the specified text. + /// Matches messages where the Bcc header contains the specified text. + /// This is equivalent to the BCC search key as defined in rfc3501. /// /// A . /// The text to match against. /// - /// is null. + /// is . /// /// /// is empty. @@ -152,12 +182,13 @@ public static TextSearchQuery BccContains (string text) /// Match messages where the message body contains the specified text. /// /// - /// Matches messages where the message body contains the specified text. + /// Matches messages where the message body contains the specified text. + /// This is equivalent to the BODY search key as defined in rfc3501. /// /// A . /// The text to match against. /// - /// is null. + /// is . /// /// /// is empty. @@ -171,12 +202,13 @@ public static TextSearchQuery BodyContains (string text) /// Match messages where the Cc header contains the specified text. /// /// - /// Matches messages where the Cc header contains the specified text. + /// Matches messages where the Cc header contains the specified text. + /// This is equivalent to the CC search key as defined in rfc3501. /// /// A . /// The text to match against. /// - /// is null. + /// is . /// /// /// is empty. @@ -190,7 +222,11 @@ public static TextSearchQuery CcContains (string text) /// Match messages that have mod-sequence values greater than or equal to the specified mod-sequence value. /// /// - /// Matches messages that have mod-sequence values greater than or equal to the specified mod-sequence value. + /// Matches messages that have mod-sequence values greater than or equal to the specified mod-sequence value. + /// This is equivalent to the MODSEQ search key as defined in rfc4551 + /// and is therefor only available for use with IMAP servers that support the CONDSTORE extension. + /// + /// /// /// A . /// The mod-sequence value. @@ -203,7 +239,8 @@ public static SearchQuery ChangedSince (ulong modseq) /// Match messages with the flag set. /// /// - /// Matches messages with the flag set. + /// Matches messages with the flag set. + /// This is equivalent to the DELETED search key as defined in rfc3501. /// public static readonly SearchQuery Deleted = new SearchQuery (SearchTerm.Deleted); @@ -211,7 +248,9 @@ public static SearchQuery ChangedSince (ulong modseq) /// Match messages that were delivered after the specified date. /// /// - /// Matches messages that were delivered after the specified date. + /// Matches messages that were delivered after the specified date. + /// The resolution of this search query does not include the time. + /// This is equivalent to the SINCE search key as defined in rfc3501. /// /// A . /// The date. @@ -224,7 +263,9 @@ public static DateSearchQuery DeliveredAfter (DateTime date) /// Match messages that were delivered before the specified date. /// /// - /// Matches messages that were delivered before the specified date. + /// Matches messages that were delivered before the specified date. + /// The resolution of this search query does not include the time. + /// This is equivalent to the BEFORE search key as defined in rfc3501. /// /// A . /// The date. @@ -237,7 +278,9 @@ public static DateSearchQuery DeliveredBefore (DateTime date) /// Match messages that were delivered on the specified date. /// /// - /// Matches messages that were delivered on the specified date. + /// Matches messages that were delivered on the specified date. + /// The resolution of this search query does not include the time. + /// This is equivalent to the ON search key as defined in rfc3501. /// /// A . /// The date. @@ -247,55 +290,129 @@ public static DateSearchQuery DeliveredOn (DateTime date) } /// - /// Match messages that do not have the specified custom flag set. + /// Match messages with the flag set. + /// + /// + /// Matches messages with the flag set. + /// This is equivalent to the DRAFT search key as defined in rfc3501. + /// + public static readonly SearchQuery Draft = new SearchQuery (SearchTerm.Draft); + + /// + /// Match messages using a saved search filter. + /// + /// + /// Matches messages using a saved search filter. + /// This is equivalent to the FILTER search key as defined in rfc5466 and is therefor only available + /// for use with IMAP servers that support the FILTERS extension. + /// + /// + /// + /// A . + /// The name of the saved search. + public static SearchQuery Filter (string name) + { + return new FilterSearchQuery (name); + } + + /// + /// Match messages using a saved search filter. + /// + /// + /// Matches messages using a saved search filter. + /// This is equivalent to the FILTER search key as defined in rfc5466 and is therefor only available + /// for use with IMAP servers that support the FILTERS extension. + /// + /// + /// + /// A . + /// The name of the saved search. + public static SearchQuery Filter (MetadataTag filter) + { + return new FilterSearchQuery (filter); + } + + /// + /// Match messages with the flag set. + /// + /// + /// Matches messages with the flag set. + /// This is equivalent to the FLAGGED search key as defined in rfc3501. + /// + public static readonly SearchQuery Flagged = new SearchQuery (SearchTerm.Flagged); + + /// + /// Match messages where the From header contains the specified text. /// /// - /// Matches messages that do not have the specified custom flag set. + /// Matches messages where the From header contains the specified text. + /// This is equivalent to the FROM search key as defined in rfc3501. /// /// A . - /// The custom flag. + /// The text to match against. /// - /// is null. + /// is . /// /// - /// is empty. + /// is empty. /// - public static TextSearchQuery DoesNotHaveCustomFlag (string flag) + public static TextSearchQuery FromContains (string text) { - if (flag == null) - throw new ArgumentNullException (nameof (flag)); + return new TextSearchQuery (SearchTerm.FromContains, text); + } - if (flag.Length == 0) - throw new ArgumentException ("Cannot search for an empty string."); + /// + /// Apply a fuzzy matching algorithm to the specified expression. + /// + /// + /// Applies a fuzzy matching algorithm to the specified expression. + /// This is equivalent to the OLDER search key as defined in rfc6203 and is therefor only available + /// for use with IMAP servers that support the SEARCH=FUZZY extension. + /// + /// + /// + /// A . + /// The expression + /// + /// is . + /// + public static UnarySearchQuery Fuzzy (SearchQuery expr) + { + if (expr == null) + throw new ArgumentNullException (nameof (expr)); - return new TextSearchQuery (SearchTerm.NotKeyword, flag); + return new UnarySearchQuery (SearchTerm.Fuzzy, expr); } /// - /// Match messages that do not have the specified custom flags set. + /// Match messages that have the specified flags set. /// /// - /// Matches messages that do not have the specified custom flags set. + /// Matches messages that have the specified flag(s) set. + /// Maps each flag to the corresponding search key (ANSWERED, DELETED, DRAFT, FLAGGED, + /// RECENT or SEEN) as defined in rfc3501. /// /// A . - /// The custom flags. - /// - /// is null. - /// + /// The message flags. /// - /// One or more of the is null or empty. - /// -or- - /// No custom flags were given. + /// does not specify any valid message flags. /// - public static SearchQuery DoesNotHaveCustomFlags (IEnumerable flags) + public static SearchQuery HasFlags (MessageFlags flags) { - if (flags == null) - throw new ArgumentNullException (nameof (flags)); - var list = new List (); - foreach (var flag in flags) - list.Add (new TextSearchQuery (SearchTerm.NotKeyword, flag)); + if ((flags & MessageFlags.Seen) != 0) + list.Add (Seen); + if ((flags & MessageFlags.Answered) != 0) + list.Add (Answered); + if ((flags & MessageFlags.Flagged) != 0) + list.Add (Flagged); + if ((flags & MessageFlags.Deleted) != 0) + list.Add (Deleted); + if ((flags & MessageFlags.Draft) != 0) + list.Add (Draft); + if ((flags & MessageFlags.Recent) != 0) + list.Add (Recent); if (list.Count == 0) throw new ArgumentException ("No flags specified.", nameof (flags)); @@ -308,17 +425,19 @@ public static SearchQuery DoesNotHaveCustomFlags (IEnumerable flags) } /// - /// Match messages that do not have the specified flags set. + /// Match messages that do not have any of the specified flags set. /// /// - /// Matches messages that do not have the specified flags set. + /// Matches messages that do not have any of the specified flags set. + /// Maps each flag to the corresponding search key (UNANSWERED, UNDELETED, UNDRAFT, UNFLAGGED, + /// OLD or UNSEEN) as defined in rfc3501. /// /// A . /// The message flags. /// - /// does not contain any of the valie flag values. + /// does not specify any valid message flags. /// - public static SearchQuery DoesNotHaveFlags (MessageFlags flags) + public static SearchQuery NotFlags (MessageFlags flags) { var list = new List (); @@ -346,164 +465,181 @@ public static SearchQuery DoesNotHaveFlags (MessageFlags flags) } /// - /// Match messages with the flag set. + /// Match messages that have the specified keyword set. /// /// - /// Matches messages with the flag set. + /// Matches messages that have the specified keyword set. + /// A keyword is a user-defined message flag that can be set (or unset) on a message. + /// This is equivalent to the KEYWORD search key as defined in rfc3501. /// - public static readonly SearchQuery Draft = new SearchQuery (SearchTerm.Draft); - - /// - /// Match messages using a saved search filter. - /// - /// - /// Matches messages using a saved search filter. - /// - /// A . - /// The name of the saved search. - public static SearchQuery Filter (string name) + /// A . + /// The keyword. + /// + /// is . + /// + /// + /// is empty. + /// + public static TextSearchQuery HasKeyword (string keyword) { - return new FilterSearchQuery (name); - } + if (keyword == null) + throw new ArgumentNullException (nameof (keyword)); - /// - /// Match messages with the flag set. - /// - /// - /// Matches messages with the flag set. - /// - public static readonly SearchQuery Flagged = new SearchQuery (SearchTerm.Flagged); + if (keyword.Length == 0) + throw new ArgumentException ("The keyword cannot be an empty string.", nameof (keyword)); + + return new TextSearchQuery (SearchTerm.Keyword, keyword); + } /// - /// Match messages where the From header contains the specified text. + /// Match messages that have all of the specified keywords set. /// /// - /// Matches messages where the From header contains the specified text. + /// Matches messages that have all of the specified keywords set. + /// A keyword is a user-defined message flag that can be set (or unset) on a message. + /// This is equivalent to AND-ing multiple KEYWORD search keys as defined in rfc3501. /// - /// A . - /// The text to match against. + /// A . + /// The keywords. /// - /// is null. + /// is . /// /// - /// is empty. + /// One or more of the is or empty. + /// -or- + /// No keywords were given. /// - public static TextSearchQuery FromContains (string text) + public static SearchQuery HasKeywords (params string[] keywords) { - return new TextSearchQuery (SearchTerm.FromContains, text); + return HasKeywords ((IEnumerable) keywords); } /// - /// Apply a fuzzy matching algorithm to the specified expression. + /// Match messages that have all of the specified keywords set. /// /// - /// Applies a fuzzy matching algorithm to the specified expression. - /// This feature is not supported by all IMAP servers. + /// Matches messages that have all of the specified keywords set. + /// A keyword is a user-defined message flag that can be set (or unset) on a message. + /// This is equivalent to AND-ing multiple KEYWORD search keys as defined in rfc3501. /// - /// A . - /// The expression + /// A . + /// The keywords. /// - /// is null. + /// is . /// - public static UnarySearchQuery Fuzzy (SearchQuery expr) + /// + /// One or more of the is or empty. + /// -or- + /// No keywords were given. + /// + public static SearchQuery HasKeywords (IEnumerable keywords) { - if (expr == null) - throw new ArgumentNullException (nameof (expr)); + if (keywords == null) + throw new ArgumentNullException (nameof (keywords)); - return new UnarySearchQuery (SearchTerm.Fuzzy, expr); + var list = new List (); + + foreach (var keyword in keywords) { + if (string.IsNullOrEmpty (keyword)) + throw new ArgumentException ("Cannot search for null or empty keywords.", nameof (keywords)); + + list.Add (new TextSearchQuery (SearchTerm.Keyword, keyword)); + } + + if (list.Count == 0) + throw new ArgumentException ("No keywords specified.", nameof (keywords)); + + var query = list[0]; + for (int i = 1; i < list.Count; i++) + query = query.And (list[i]); + + return query; } /// - /// Match messages that have the specified custom flag set. + /// Match messages that do not have the specified keyword set. /// /// - /// Matches messages that have the specified custom flag set. + /// Matches messages that do not have the specified keyword set. + /// A keyword is a user-defined message flag that can be set (or unset) on a message. + /// This is equivalent to the UNKEYWORD search key as defined in rfc3501. /// /// A . - /// The custom flag. + /// The keyword. /// - /// is null. + /// is . /// /// - /// is empty. + /// is empty. /// - public static TextSearchQuery HasCustomFlag (string flag) + public static TextSearchQuery NotKeyword (string keyword) { - if (flag == null) - throw new ArgumentNullException (nameof (flag)); + if (keyword == null) + throw new ArgumentNullException (nameof (keyword)); - if (flag.Length == 0) - throw new ArgumentException ("Cannot search for an empty string."); + if (keyword.Length == 0) + throw new ArgumentException ("The keyword cannot be an empty string.", nameof (keyword)); - return new TextSearchQuery (SearchTerm.Keyword, flag); + return new TextSearchQuery (SearchTerm.NotKeyword, keyword); } /// - /// Match messages that have the specified custom flags set. + /// Match messages that do not have any of the specified keywords set. /// /// - /// Matches messages that have the specified custom flags set. + /// Matches messages that do not have any of the specified keywords set. + /// A keyword is a user-defined message flag that can be set (or unset) on a message. + /// This is equivalent to AND-ing multiple UNKEYWORD search keys as defined in rfc3501. /// /// A . - /// The custom flags. + /// The keywords. /// - /// is null. + /// is . /// /// - /// One or more of the is null or empty. + /// One or more of the is or empty. /// -or- - /// No custom flags were given. + /// No keywords were given. /// - public static SearchQuery HasCustomFlags (IEnumerable flags) + public static SearchQuery NotKeywords (params string[] keywords) { - if (flags == null) - throw new ArgumentNullException (nameof (flags)); - - var list = new List (); - - foreach (var flag in flags) - list.Add (new TextSearchQuery (SearchTerm.Keyword, flag)); - - if (list.Count == 0) - throw new ArgumentException ("No flags specified.", nameof (flags)); - - var query = list[0]; - for (int i = 1; i < list.Count; i++) - query = query.And (list[i]); - - return query; + return NotKeywords ((IEnumerable) keywords); } /// - /// Match messages that have the specified flags set. + /// Match messages that do not have any of the specified keywords set. /// /// - /// Matches messages that have the specified flags set. + /// Matches messages that do not have any of the specified keywords set. + /// A keyword is a user-defined message flag that can be set (or unset) on a message. + /// This is equivalent to AND-ing multiple UNKEYWORD search keys as defined in rfc3501. /// /// A . - /// The message flags. + /// The keywords. + /// + /// is . + /// /// - /// does not contain any of the valie flag values. + /// One or more of the is or empty. + /// -or- + /// No keywords were given. /// - public static SearchQuery HasFlags (MessageFlags flags) + public static SearchQuery NotKeywords (IEnumerable keywords) { + if (keywords == null) + throw new ArgumentNullException (nameof (keywords)); + var list = new List (); - if ((flags & MessageFlags.Seen) != 0) - list.Add (Seen); - if ((flags & MessageFlags.Answered) != 0) - list.Add (Answered); - if ((flags & MessageFlags.Flagged) != 0) - list.Add (Flagged); - if ((flags & MessageFlags.Deleted) != 0) - list.Add (Deleted); - if ((flags & MessageFlags.Draft) != 0) - list.Add (Draft); - if ((flags & MessageFlags.Recent) != 0) - list.Add (Recent); + foreach (var keyword in keywords) { + if (string.IsNullOrEmpty (keyword)) + throw new ArgumentException ("Cannot search for null or empty keywords.", nameof (keywords)); + + list.Add (new TextSearchQuery (SearchTerm.NotKeyword, keyword)); + } if (list.Count == 0) - throw new ArgumentException ("No flags specified.", nameof (flags)); + throw new ArgumentException ("No flags specified.", nameof (keywords)); var query = list[0]; for (int i = 1; i < list.Count; i++) @@ -516,40 +652,16 @@ public static SearchQuery HasFlags (MessageFlags flags) /// Match messages where the specified header contains the specified text. /// /// - /// Matches messages where the specified header contains the specified text. + /// Matches messages where the specified header contains the specified text. + /// This is equivalent to the HEADER search key as defined in rfc3501. /// /// A . /// The header field to match against. /// The text to match against. /// - /// is null. + /// is . /// -or- - /// is null. - /// - /// - /// is empty. - /// -or- - /// is empty. - /// - [Obsolete ("Use HeaderContains(string, string) instead.")] - public static HeaderSearchQuery Header (string field, string text) - { - return HeaderContains (field, text); - } - - /// - /// Match messages where the specified header contains the specified text. - /// - /// - /// Matches messages where the specified header contains the specified text. - /// - /// A . - /// The header field to match against. - /// The text to match against. - /// - /// is null. - /// -or- - /// is null. + /// is . /// /// /// is empty. @@ -572,7 +684,8 @@ public static HeaderSearchQuery HeaderContains (string field, string text) /// Match messages that are larger than the specified number of octets. /// /// - /// Matches messages that are larger than the specified number of octets. + /// Matches messages that are larger than the specified number of octets. + /// This is equivalent to the LARGER search key as defined in rfc3501. /// /// A . /// The number of octets. @@ -588,15 +701,16 @@ public static NumericSearchQuery LargerThan (int octets) } /// - /// Match messages where the raw message contains the specified text. + /// Match messages that contain the specified text in either the header or the body. /// /// - /// Matches messages where the raw message contains the specified text. + /// Matches messages that contain the specified text in either the header or the body. + /// This is equivalent to the TEXT search key as defined in rfc3501. /// /// A . /// The text to match against. /// - /// is null. + /// is . /// /// /// is empty. @@ -610,7 +724,8 @@ public static TextSearchQuery MessageContains (string text) /// Match messages with the flag set but not the . /// /// - /// Matches messages with the flag set but not the . + /// Matches messages with the flag set but not the . + /// This is equivalent to the NEW search key as defined in rfc3501. /// public static readonly SearchQuery New = new SearchQuery (SearchTerm.New); @@ -618,12 +733,13 @@ public static TextSearchQuery MessageContains (string text) /// Create a logical negation of the specified expression. /// /// - /// Creates a logical negation of the specified expression. + /// Creates a logical negation of the specified expression. + /// This is equivalent to the NOT search key as defined in rfc3501. /// /// A . /// The expression /// - /// is null. + /// is . /// public static UnarySearchQuery Not (SearchQuery expr) { @@ -637,7 +753,8 @@ public static UnarySearchQuery Not (SearchQuery expr) /// Match messages that do not have the flag set. /// /// - /// Matches messages that do not have the flag set. + /// Matches messages that do not have the flag set. + /// This is equivalent to the UNANSWERED search key as defined in rfc3501. /// public static readonly SearchQuery NotAnswered = new SearchQuery (SearchTerm.NotAnswered); @@ -645,7 +762,8 @@ public static UnarySearchQuery Not (SearchQuery expr) /// Match messages that do not have the flag set. /// /// - /// Matches messages that do not have the flag set. + /// Matches messages that do not have the flag set. + /// This is equivalent to the UNDELETED search key as defined in rfc3501. /// public static readonly SearchQuery NotDeleted = new SearchQuery (SearchTerm.NotDeleted); @@ -653,7 +771,8 @@ public static UnarySearchQuery Not (SearchQuery expr) /// Match messages that do not have the flag set. /// /// - /// Matches messages that do not have the flag set. + /// Matches messages that do not have the flag set. + /// This is equivalent to the UNDRAFT search key as defined in rfc3501. /// public static readonly SearchQuery NotDraft = new SearchQuery (SearchTerm.NotDraft); @@ -661,7 +780,8 @@ public static UnarySearchQuery Not (SearchQuery expr) /// Match messages that do not have the flag set. /// /// - /// Matches messages that do not have the flag set. + /// Matches messages that do not have the flag set. + /// This is equivalent to the UNFLAGGED search key as defined in rfc3501. /// public static readonly SearchQuery NotFlagged = new SearchQuery (SearchTerm.NotFlagged); @@ -669,7 +789,8 @@ public static UnarySearchQuery Not (SearchQuery expr) /// Match messages that do not have the flag set. /// /// - /// Matches messages that do not have the flag set. + /// Matches messages that do not have the flag set. + /// This is equivalent to the OLD search key as defined in rfc3501. /// public static readonly SearchQuery NotRecent = new SearchQuery (SearchTerm.NotRecent); @@ -677,15 +798,29 @@ public static UnarySearchQuery Not (SearchQuery expr) /// Match messages that do not have the flag set. /// /// - /// Matches messages that do not have the flag set. + /// Matches messages that do not have the flag set. + /// This is equivalent to the UNSEEN search key as defined in rfc3501. /// public static readonly SearchQuery NotSeen = new SearchQuery (SearchTerm.NotSeen); + /// + /// Match messages that do not have the flag set. + /// + /// + /// Matches messages that do not have the flag set. + /// This is equivalent to the OLD search key as defined in rfc3501. + /// + public static readonly SearchQuery Old = new SearchQuery (SearchTerm.NotRecent); + /// /// Match messages older than the specified number of seconds. /// /// - /// Matches messages older than the specified number of seconds. + /// Matches messages older than the specified number of seconds. + /// This is equivalent to the OLDER search key as defined in rfc5032 and is therefor only available + /// for use with IMAP servers that support the WITHIN extension. + /// + /// /// /// A . /// The number of seconds. @@ -704,15 +839,16 @@ public static NumericSearchQuery OlderThan (int seconds) /// Create a conditional OR operation. /// /// - /// A conditional OR operation only evaluates the second operand if the first operand evaluates to false. + /// A conditional OR operation only evaluates the second operand if the first operand evaluates to false. + /// This is equivalent to the OR search key as defined in rfc3501. /// /// A representing the conditional OR operation. /// The first operand. /// The second operand. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// public static BinarySearchQuery Or (SearchQuery left, SearchQuery right) { @@ -729,12 +865,13 @@ public static BinarySearchQuery Or (SearchQuery left, SearchQuery right) /// Create a conditional OR operation. /// /// - /// A conditional OR operation only evaluates the second operand if the first operand evaluates to true. + /// A conditional OR operation only evaluates the second operand if the first operand evaluates to true. + /// This is equivalent to the OR search key as defined in rfc3501. /// /// A representing the conditional AND operation. /// An additional query to execute. /// - /// is null. + /// is . /// public BinarySearchQuery Or (SearchQuery expr) { @@ -748,36 +885,96 @@ public BinarySearchQuery Or (SearchQuery expr) /// Match messages with the flag set. /// /// - /// Matches messages with the flag set. + /// Matches messages with the flag set. + /// This is equivalent to the RECENT search key as defined in rfc3501. /// public static readonly SearchQuery Recent = new SearchQuery (SearchTerm.Recent); /// - /// Match messages with the flag set. + /// Match all messages in the mailbox when the underlying storage of that mailbox supports + /// the save date attribute. /// /// - /// Matches messages with the flag set. + /// Matches all messages in the mailbox when the underlying storage of that mailbox supports + /// the save date attribute. Conversely, it matches no messages in the mailbox when the save + /// date attribute is not supported. + /// This is equivalent to the SAVEDATESUPPORTED search key as defined in rfc8514, section 4.3 + /// and is therefor only available for use with IMAP servers that support the SAVEDATE extension. + /// + /// /// - public static readonly SearchQuery Seen = new SearchQuery (SearchTerm.Seen); + public static readonly SearchQuery SaveDateSupported = new SearchQuery (SearchTerm.SaveDateSupported); /// - /// Match messages that were sent after the specified date. + /// Match messages that were saved to the mailbox before the specified date. /// /// - /// Matches messages that were sent after the specified date. + /// Matches messages that were saved to the mailbox before the specified date. + /// The resolution of this search query does not include the time. + /// This is equivalent to the SAVEDBEFORE search key as defined in rfc8514, section 4.3 + /// and is therefor only available for use with IMAP servers that support the SAVEDATE extension. + /// + /// /// /// A . /// The date. - public static DateSearchQuery SentAfter (DateTime date) + public static DateSearchQuery SavedBefore (DateTime date) { - return new DateSearchQuery (SearchTerm.SentAfter, date); + return new DateSearchQuery (SearchTerm.SavedBefore, date); } + /// + /// Match messages that were saved to the mailbox on the specified date. + /// + /// + /// Matches messages that were saved to the mailbox on the specified date. + /// The resolution of this search query does not include the time. + /// This is equivalent to the SAVEDON search key as defined in rfc8514, section 4.3 + /// and is therefor only available for use with IMAP servers that support the SAVEDATE extension. + /// + /// + /// + /// A . + /// The date. + public static DateSearchQuery SavedOn (DateTime date) + { + return new DateSearchQuery (SearchTerm.SavedOn, date); + } + + /// + /// Match messages that were saved to the mailbox since the specified date. + /// + /// + /// Matches messages that were saved to the mailbox since the specified date. + /// The resolution of this search query does not include the time. + /// This is equivalent to the SAVEDSINCE search key as defined in rfc8514, section 4.3 + /// and is therefor only available for use with IMAP servers that support the SAVEDATE extension. + /// + /// + /// + /// A . + /// The date. + public static DateSearchQuery SavedSince (DateTime date) + { + return new DateSearchQuery (SearchTerm.SavedSince, date); + } + + /// + /// Match messages with the flag set. + /// + /// + /// Matches messages with the flag set. + /// This is equivalent to the SEEN search key as defined in rfc3501. + /// + public static readonly SearchQuery Seen = new SearchQuery (SearchTerm.Seen); + /// /// Match messages that were sent before the specified date. /// /// - /// Matches messages that were sent before the specified date. + /// Matches messages that were sent before the specified date. + /// The resolution of this search query does not include the time. + /// This is equivalent to the SENTBEFORE search key as defined in rfc3501. /// /// A . /// The date. @@ -790,7 +987,9 @@ public static DateSearchQuery SentBefore (DateTime date) /// Match messages that were sent on the specified date. /// /// - /// Matches messages that were sent on the specified date. + /// Matches messages that were sent on the specified date. + /// The resolution of this search query does not include the time. + /// This is equivalent to the SENTON search key as defined in rfc3501. /// /// A . /// The date. @@ -799,11 +998,27 @@ public static DateSearchQuery SentOn (DateTime date) return new DateSearchQuery (SearchTerm.SentOn, date); } + /// + /// Match messages that were sent since the specified date. + /// + /// + /// Matches messages that were sent since the specified date. + /// The resolution of this search query does not include the time. + /// This is equivalent to the SENTSINCE search key as defined in rfc3501. + /// + /// A . + /// The date. + public static DateSearchQuery SentSince (DateTime date) + { + return new DateSearchQuery (SearchTerm.SentSince, date); + } + /// /// Match messages that are smaller than the specified number of octets. /// /// - /// Matches messages that are smaller than the specified number of octets. + /// Matches messages that are smaller than the specified number of octets. + /// This is equivalent to the SMALLER search key as defined in rfc3501. /// /// A . /// The number of octets. @@ -822,12 +1037,13 @@ public static NumericSearchQuery SmallerThan (int octets) /// Match messages where the Subject header contains the specified text. /// /// - /// Matches messages where the Subject header contains the specified text. + /// Matches messages where the Subject header contains the specified text. + /// This is equivalent to the SUBJECT search key as defined in rfc3501. /// /// A . /// The text to match against. /// - /// is null. + /// is . /// /// /// is empty. @@ -841,12 +1057,13 @@ public static TextSearchQuery SubjectContains (string text) /// Match messages where the To header contains the specified text. /// /// - /// Matches messages where the To header contains the specified text. + /// Matches messages where the To header contains the specified text. + /// This is equivalent to the TO search key as defined in rfc3501. /// /// A . /// The text to match against. /// - /// is null. + /// is . /// /// /// is empty. @@ -860,12 +1077,13 @@ public static TextSearchQuery ToContains (string text) /// Limit the search query to messages with the specified unique identifiers. /// /// - /// Limits the search query to messages with the specified unique identifiers. + /// Limits the search query to messages with the specified unique identifiers. + /// This is equivalent to the UID search key as defined in rfc3501. /// /// A . /// The unique identifiers. /// - /// is null. + /// is . /// /// /// is empty. @@ -879,7 +1097,11 @@ public static UidSearchQuery Uids (IList uids) /// Match messages younger than the specified number of seconds. /// /// - /// Matches messages younger than the specified number of seconds. + /// Matches messages younger than the specified number of seconds. + /// This is equivalent to the YOUNGER search key as defined in rfc5032 and is therefor only available + /// for use with IMAP servers that support the WITHIN extension. + /// + /// /// /// A . /// The number of seconds. @@ -900,7 +1122,11 @@ public static NumericSearchQuery YoungerThan (int seconds) /// Match messages that have the specified GMail message identifier. /// /// - /// This search term can only be used with GMail. + /// Matches messages that have the specified GMail message identifier. + /// This is equivalent to the X-GM-MSGID search key as defined in Google's IMAP extensions and is therefor only available + /// for use with IMAP servers that support the X-GM-EXT1 extension. + /// + /// /// /// A . /// The GMail message identifier. @@ -913,7 +1139,11 @@ public static NumericSearchQuery GMailMessageId (ulong id) /// Match messages belonging to the specified GMail thread. /// /// - /// This search term can only be used with GMail. + /// Matches messages belonging to the specified GMail thread. + /// This is equivalent to the X-GM-THRID search key as defined in Google's IMAP extensions and is therefor only available + /// for use with IMAP servers that support the X-GM-EXT1 extension. + /// + /// /// /// A . /// The GMail thread. @@ -926,12 +1156,16 @@ public static NumericSearchQuery GMailThreadId (ulong thread) /// Match messages that have the specified GMail label. /// /// - /// This search term can only be used with GMail. + /// Matches messages that have the specified GMail label. + /// This is equivalent to the X-GM-LABELS search key as defined in Google's IMAP extensions and is therefor only available + /// for use with IMAP servers that support the X-GM-EXT1 extension. + /// + /// /// /// A . /// The GMail label. /// - /// is null. + /// is . /// /// /// is empty. @@ -951,12 +1185,16 @@ public static TextSearchQuery HasGMailLabel (string label) /// Match messages using the GMail search expression. /// /// - /// This search term can only be used with GMail. + /// Matches messages using Google's custom message search syntax. + /// This is equivalent to the X-GM-RAW search key as defined in Google's IMAP extensions and is therefor only available + /// for use with IMAP servers that support the X-GM-EXT1 extension. + /// + /// /// /// A . /// The raw GMail search text. /// - /// is null. + /// is . /// /// /// is empty. diff --git a/MailKit/Search/SearchResults.cs b/MailKit/Search/SearchResults.cs index 32ce71490a..d9a647f4b4 100644 --- a/MailKit/Search/SearchResults.cs +++ b/MailKit/Search/SearchResults.cs @@ -1,9 +1,9 @@ -// +// // SearchResults.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -41,9 +41,23 @@ public class SearchResults /// /// Creates a new . /// - public SearchResults () + /// The UID validity value. + /// The sort-order to use for the unique identifiers. + public SearchResults (uint uidValidity, SortOrder order = SortOrder.None) + { + UniqueIds = new UniqueIdSet (uidValidity, order); + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The sort-order to use for the unique identifiers. + public SearchResults (SortOrder order = SortOrder.None) { - UniqueIds = new UniqueId[0]; + UniqueIds = new UniqueIdSet (order); } /// @@ -108,7 +122,7 @@ public ulong? ModSeq { /// Gets or sets the relevancy scores of the messages that matched the search query. /// /// The relevancy scores. - public IList Relevancy { + public IList? Relevancy { get; set; } } diff --git a/MailKit/Search/SearchTerm.cs b/MailKit/Search/SearchTerm.cs index f71912992b..4fcbc948be 100644 --- a/MailKit/Search/SearchTerm.cs +++ b/MailKit/Search/SearchTerm.cs @@ -1,9 +1,9 @@ -// +// // SearchTerm.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -44,6 +44,11 @@ public enum SearchTerm { /// And, + /// + /// A search term that matches messages that have the specified annotation. + /// + Annotation, + /// /// A search term that matches answered messages. /// @@ -209,14 +214,31 @@ public enum SearchTerm { Recent, /// - /// A search term that matches messages that have been seen. + /// A search term that matches all messages in the mailbox when the underlying storage of + /// that mailbox supports the save date attribute. Conversely, it matches no messages in + /// the mailbox when the save date attribute is not supported. /// - Seen, + SaveDateSupported, + + /// + /// A search term that matches messages that were saved to the mailbox before a specified date. + /// + SavedBefore, + + /// + /// A search term that matches messages that were saved to the mailbox on a specified date. + /// + SavedOn, + + /// + /// A search term that matches messages that were saved to the mailbox since a specified date. + /// + SavedSince, /// - /// A search term that matches messages that were sent after a specified date. + /// A search term that matches messages that have been seen. /// - SentAfter, + Seen, /// /// A search term that matches messages that were sent before a specified date. @@ -228,6 +250,11 @@ public enum SearchTerm { /// SentOn, + /// + /// A search term that matches messages that were sent since a specified date. + /// + SentSince, + /// /// A search term that matches messages that are smaller than a /// specified number of bytes. diff --git a/MailKit/Search/SortOrder.cs b/MailKit/Search/SortOrder.cs index 8115f9646d..77d962d8e8 100644 --- a/MailKit/Search/SortOrder.cs +++ b/MailKit/Search/SortOrder.cs @@ -1,9 +1,9 @@ -// +// // SortOrder.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal diff --git a/MailKit/Search/TextSearchQuery.cs b/MailKit/Search/TextSearchQuery.cs index 6a87af7ebf..1e085f97ff 100644 --- a/MailKit/Search/TextSearchQuery.cs +++ b/MailKit/Search/TextSearchQuery.cs @@ -1,9 +1,9 @@ -// +// // TextSearchQuery.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -45,7 +45,7 @@ public class TextSearchQuery : SearchQuery /// The search term. /// The text to match against. /// - /// is null. + /// is . /// /// /// is empty. diff --git a/MailKit/Search/UidSearchQuery.cs b/MailKit/Search/UidSearchQuery.cs index 4c4e25e0d3..4d73587d42 100644 --- a/MailKit/Search/UidSearchQuery.cs +++ b/MailKit/Search/UidSearchQuery.cs @@ -1,9 +1,9 @@ -// +// // UidSearchQuery.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -45,7 +45,7 @@ public class UidSearchQuery : SearchQuery /// /// The unique identifiers to match against. /// - /// is null. + /// is . /// /// /// is empty. @@ -76,8 +76,9 @@ public UidSearchQuery (UniqueId uid) : base (SearchTerm.Uid) if (!uid.IsValid) throw new ArgumentException ("Cannot search for an invalid unique identifier.", nameof (uid)); - Uids = new UniqueIdSet (SortOrder.Ascending); - Uids.Add (uid); + Uids = new UniqueIdSet (SortOrder.Ascending) { + uid + }; } /// diff --git a/MailKit/Search/UnarySearchQuery.cs b/MailKit/Search/UnarySearchQuery.cs index 27840a692f..bf0b4fcad9 100644 --- a/MailKit/Search/UnarySearchQuery.cs +++ b/MailKit/Search/UnarySearchQuery.cs @@ -1,9 +1,9 @@ -// +// // UnarySearchQuery.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -45,7 +45,7 @@ public class UnarySearchQuery : SearchQuery /// The search term. /// The operand. /// - /// is null. + /// is . /// public UnarySearchQuery (SearchTerm term, SearchQuery operand) : base (term) { diff --git a/MailKit/Security/AuthenticationException.cs b/MailKit/Security/AuthenticationException.cs index d39ec85335..7eee2546ff 100644 --- a/MailKit/Security/AuthenticationException.cs +++ b/MailKit/Security/AuthenticationException.cs @@ -1,9 +1,9 @@ -// +// // AuthenticationException.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -46,13 +46,14 @@ public class AuthenticationException : Exception /// Initializes a new instance of the class. /// /// - /// Creates a new from the seriaized data. + /// Creates a new from the serialized data. /// /// The serialization info. /// The streaming context. /// - /// is null. + /// is . /// + [Obsolete ("This API supports obsolete formatter-based serialization. It should not be called or extended by application code.")] protected AuthenticationException (SerializationInfo info, StreamingContext context) : base (info, context) { } diff --git a/MailKit/Security/KeyedHashAlgorithm.cs b/MailKit/Security/KeyedHashAlgorithm.cs index 66eab74933..97e46f221f 100644 --- a/MailKit/Security/KeyedHashAlgorithm.cs +++ b/MailKit/Security/KeyedHashAlgorithm.cs @@ -3,7 +3,7 @@ // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2019 Xamarin Inc. (www.xamarin.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal diff --git a/MailKit/Security/Ntlm/BitConverterLE.cs b/MailKit/Security/Ntlm/BitConverterLE.cs index 6ca03ea41d..331ea9747a 100644 --- a/MailKit/Security/Ntlm/BitConverterLE.cs +++ b/MailKit/Security/Ntlm/BitConverterLE.cs @@ -26,56 +26,15 @@ using System; -namespace MailKit.Security.Ntlm -{ - sealed class BitConverterLE +namespace MailKit.Security.Ntlm { + static class BitConverterLE { - BitConverterLE () - { - } - - unsafe static byte[] GetUShortBytes (byte *bytes) - { - if (BitConverter.IsLittleEndian) - return new [] { bytes [0], bytes [1] }; - - return new [] { bytes [1], bytes [0] }; - } - - unsafe static byte[] GetUIntBytes (byte *bytes) - { - if (BitConverter.IsLittleEndian) - return new [] { bytes [0], bytes [1], bytes [2], bytes [3] }; - - return new [] { bytes [3], bytes [2], bytes [1], bytes [0] }; - } - unsafe static byte[] GetULongBytes (byte *bytes) { if (BitConverter.IsLittleEndian) - return new [] { bytes [0], bytes [1], bytes [2], bytes [3], bytes [4], bytes [5], bytes [6], bytes [7] }; - - return new [] { bytes [7], bytes [6], bytes [5], bytes [4], bytes [3], bytes [2], bytes [1], bytes [0] }; - } + return new [] { bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7] }; - unsafe internal static byte[] GetBytes (bool value) - { - return new [] { value ? (byte) 1 : (byte) 0 }; - } - - unsafe internal static byte[] GetBytes (char value) - { - return GetUShortBytes ((byte *) &value); - } - - unsafe internal static byte[] GetBytes (short value) - { - return GetUShortBytes ((byte *) &value); - } - - unsafe internal static byte[] GetBytes (int value) - { - return GetUIntBytes ((byte *) &value); + return new [] { bytes[7], bytes[6], bytes[5], bytes[4], bytes[3], bytes[2], bytes[1], bytes[0] }; } unsafe internal static byte[] GetBytes (long value) @@ -83,31 +42,6 @@ unsafe internal static byte[] GetBytes (long value) return GetULongBytes ((byte *) &value); } - unsafe internal static byte[] GetBytes (ushort value) - { - return GetUShortBytes ((byte *) &value); - } - - unsafe internal static byte[] GetBytes (uint value) - { - return GetUIntBytes ((byte *) &value); - } - - unsafe internal static byte[] GetBytes (ulong value) - { - return GetULongBytes ((byte *) &value); - } - - unsafe internal static byte[] GetBytes (float value) - { - return GetUIntBytes ((byte *) &value); - } - - unsafe internal static byte[] GetBytes (double value) - { - return GetULongBytes ((byte *) &value); - } - unsafe static void UShortFromBytes (byte *dst, byte[] src, int startIndex) { if (BitConverter.IsLittleEndian) { @@ -122,44 +56,19 @@ unsafe static void UShortFromBytes (byte *dst, byte[] src, int startIndex) unsafe static void UIntFromBytes (byte *dst, byte[] src, int startIndex) { if (BitConverter.IsLittleEndian) { - dst [0] = src[startIndex]; - dst [1] = src[startIndex + 1]; - dst [2] = src[startIndex + 2]; - dst [3] = src[startIndex + 3]; - } else { - dst [0] = src[startIndex + 3]; - dst [1] = src[startIndex + 2]; - dst [2] = src[startIndex + 1]; - dst [3] = src[startIndex]; - } - } - - unsafe static void ULongFromBytes (byte *dst, byte[] src, int startIndex) - { - if (BitConverter.IsLittleEndian) { - for (int i = 0; i < 8; ++i) - dst [i] = src [startIndex + i]; + dst[0] = src[startIndex]; + dst[1] = src[startIndex + 1]; + dst[2] = src[startIndex + 2]; + dst[3] = src[startIndex + 3]; } else { - for (int i = 0; i < 8; ++i) - dst [i] = src [startIndex + (7 - i)]; + dst[0] = src[startIndex + 3]; + dst[1] = src[startIndex + 2]; + dst[2] = src[startIndex + 1]; + dst[3] = src[startIndex]; } } - unsafe internal static bool ToBoolean (byte[] value, int startIndex) - { - return value [startIndex] != 0; - } - - unsafe internal static char ToChar (byte[] value, int startIndex) - { - char ret; - - UShortFromBytes ((byte *) &ret, value, startIndex); - - return ret; - } - - unsafe internal static short ToInt16 (byte[] value, int startIndex) + public unsafe static short ToInt16 (byte[] value, int startIndex) { short ret; @@ -168,7 +77,7 @@ unsafe internal static short ToInt16 (byte[] value, int startIndex) return ret; } - unsafe internal static int ToInt32 (byte[] value, int startIndex) + public unsafe static int ToInt32 (byte[] value, int startIndex) { int ret; @@ -177,16 +86,7 @@ unsafe internal static int ToInt32 (byte[] value, int startIndex) return ret; } - unsafe internal static long ToInt64 (byte[] value, int startIndex) - { - long ret; - - ULongFromBytes ((byte *) &ret, value, startIndex); - - return ret; - } - - unsafe internal static ushort ToUInt16 (byte[] value, int startIndex) + public unsafe static ushort ToUInt16 (byte[] value, int startIndex) { ushort ret; @@ -195,7 +95,7 @@ unsafe internal static ushort ToUInt16 (byte[] value, int startIndex) return ret; } - unsafe internal static uint ToUInt32 (byte[] value, int startIndex) + public unsafe static uint ToUInt32 (byte[] value, int startIndex) { uint ret; @@ -203,32 +103,5 @@ unsafe internal static uint ToUInt32 (byte[] value, int startIndex) return ret; } - - unsafe internal static ulong ToUInt64 (byte[] value, int startIndex) - { - ulong ret; - - ULongFromBytes ((byte *) &ret, value, startIndex); - - return ret; - } - - unsafe internal static float ToSingle (byte[] value, int startIndex) - { - float ret; - - UIntFromBytes ((byte *) &ret, value, startIndex); - - return ret; - } - - unsafe internal static double ToDouble (byte[] value, int startIndex) - { - double ret; - - ULongFromBytes ((byte *) &ret, value, startIndex); - - return ret; - } } } diff --git a/MailKit/Security/Ntlm/ChallengeResponse.cs b/MailKit/Security/Ntlm/ChallengeResponse.cs deleted file mode 100644 index c89fd9d074..0000000000 --- a/MailKit/Security/Ntlm/ChallengeResponse.cs +++ /dev/null @@ -1,228 +0,0 @@ -// -// Mono.Security.Protocol.Ntlm.ChallengeResponse -// Implements Challenge Response for NTLM v1 -// -// Authors: Sebastien Pouliot -// Jeffrey Stedfast -// -// Copyright (c) 2003 Motus Technologies Inc. (http://www.motus.com) -// Copyright (c) 2004 Novell (http://www.novell.com) -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) -// -// References -// a. NTLM Authentication Scheme for HTTP, Ronald Tschalär -// http://www.innovation.ch/java/ntlm.html -// b. The NTLM Authentication Protocol, Copyright © 2003 Eric Glass -// http://davenport.sourceforge.net/ntlm.html -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// - -using System; -using System.Text; - -#if !NETFX_CORE -using System.Security.Cryptography; -#else -using Encoding = Portable.Text.Encoding; -#endif - -namespace MailKit.Security.Ntlm { - class ChallengeResponse : IDisposable - { - static readonly byte[] magic = { 0x4B, 0x47, 0x53, 0x21, 0x40, 0x23, 0x24, 0x25 }; - - // This is the pre-encrypted magic value with a null DES key (0xAAD3B435B51404EE) - // Ref: http://packetstormsecurity.nl/Crackers/NT/l0phtcrack/l0phtcrack2.5-readme.html - static readonly byte[] nullEncMagic = { 0xAA, 0xD3, 0xB4, 0x35, 0xB5, 0x14, 0x04, 0xEE }; - - byte[] challenge, lmpwd, ntpwd; - bool disposed; - - public ChallengeResponse () - { - lmpwd = new byte[21]; - ntpwd = new byte[21]; - } - - public ChallengeResponse (string password, byte[] challenge) : this () - { - Password = password; - Challenge = challenge; - } - - ~ChallengeResponse () - { - Dispose (false); - } - - void CheckDisposed () - { - if (disposed) - throw new ObjectDisposedException (nameof (ChallengeResponse)); - } - - public string Password { - get { return null; } - set { - CheckDisposed (); - - // create Lan Manager password - using (var des = DES.Create ()) { - des.Mode = CipherMode.ECB; - - // Note: In .NET DES cannot accept a weak key - // this can happen for a null password - if (string.IsNullOrEmpty (value)) { - Buffer.BlockCopy (nullEncMagic, 0, lmpwd, 0, 8); - } else { - des.Key = PasswordToKey (value, 0); - using (var ct = des.CreateEncryptor ()) - ct.TransformBlock (magic, 0, 8, lmpwd, 0); - } - - // and if a password has less than 8 characters - if (value == null || value.Length < 8) { - Buffer.BlockCopy (nullEncMagic, 0, lmpwd, 8, 8); - } else { - des.Key = PasswordToKey (value, 7); - using (var ct = des.CreateEncryptor ()) - ct.TransformBlock (magic, 0, 8, lmpwd, 8); - } - - // create NT password - using (var md4 = new MD4 ()) { - var data = value == null ? new byte[0] : Encoding.Unicode.GetBytes (value); - var hash = md4.ComputeHash (data); - - Buffer.BlockCopy (hash, 0, ntpwd, 0, 16); - - // clean up - Array.Clear (data, 0, data.Length); - Array.Clear (hash, 0, hash.Length); - } - } - } - } - - public byte[] Challenge { - get { return null; } - set { - if (value == null) - throw new ArgumentNullException (nameof (value)); - - CheckDisposed (); - - // we don't want the caller to modify the value afterward - challenge = (byte[]) value.Clone (); - } - } - - public byte[] LM { - get { - CheckDisposed (); - - return GetResponse (lmpwd); - } - } - - public byte[] NT { - get { - CheckDisposed (); - - return GetResponse (ntpwd); - } - } - - static byte[] PrepareDESKey (byte[] key56bits, int position) - { - // convert to 8 bytes - var key = new byte[8]; - - key[0] = key56bits [position]; - key[1] = (byte) ((key56bits [position] << 7) | (key56bits [position + 1] >> 1)); - key[2] = (byte) ((key56bits [position + 1] << 6) | (key56bits [position + 2] >> 2)); - key[3] = (byte) ((key56bits [position + 2] << 5) | (key56bits [position + 3] >> 3)); - key[4] = (byte) ((key56bits [position + 3] << 4) | (key56bits [position + 4] >> 4)); - key[5] = (byte) ((key56bits [position + 4] << 3) | (key56bits [position + 5] >> 5)); - key[6] = (byte) ((key56bits [position + 5] << 2) | (key56bits [position + 6] >> 6)); - key[7] = (byte) (key56bits [position + 6] << 1); - - return key; - } - - static byte[] PasswordToKey (string password, int position) - { - int len = Math.Min (password.Length - position, 7); - var key7 = new byte[7]; - - Encoding.ASCII.GetBytes (password.ToUpper (), position, len, key7, 0); - var key8 = PrepareDESKey (key7, 0); - - Array.Clear (key7, 0, key7.Length); - - return key8; - } - - byte[] GetResponse (byte[] pwd) - { - var response = new byte[24]; - - using (var des = DES.Create ()) { - des.Mode = CipherMode.ECB; - des.Key = PrepareDESKey (pwd, 0); - - using (var transform = des.CreateEncryptor ()) - transform.TransformBlock (challenge, 0, 8, response, 0); - - des.Key = PrepareDESKey (pwd, 7); - - using (var transform = des.CreateEncryptor ()) - transform.TransformBlock (challenge, 0, 8, response, 8); - - des.Key = PrepareDESKey (pwd, 14); - - using (var transform = des.CreateEncryptor ()) - transform.TransformBlock (challenge, 0, 8, response, 16); - } - - return response; - } - - void Dispose (bool disposing) - { - if (!disposed) { - // cleanup our stuff - Array.Clear (lmpwd, 0, lmpwd.Length); - Array.Clear (ntpwd, 0, ntpwd.Length); - - if (challenge != null) - Array.Clear (challenge, 0, challenge.Length); - } - } - - public void Dispose () - { - Dispose (true); - GC.SuppressFinalize (this); - disposed = true; - } - } -} diff --git a/MailKit/Security/Ntlm/ChallengeResponse2.cs b/MailKit/Security/Ntlm/ChallengeResponse2.cs deleted file mode 100644 index 08ebf92406..0000000000 --- a/MailKit/Security/Ntlm/ChallengeResponse2.cs +++ /dev/null @@ -1,287 +0,0 @@ -// -// Mono.Security.Protocol.Ntlm.ChallengeResponse -// Implements Challenge Response for NTLM v1 and NTLM v2 Session -// -// Authors: Sebastien Pouliot -// Martin Baulig -// Jeffrey Stedfast -// -// Copyright (c) 2003 Motus Technologies Inc. (http://www.motus.com) -// Copyright (c) 2004 Novell (http://www.novell.com) -// Copyright (c) 2012 Xamarin, Inc. (http://www.xamarin.com) -// -// References -// a. NTLM Authentication Scheme for HTTP, Ronald Tschalär -// http://www.innovation.ch/java/ntlm.html -// b. The NTLM Authentication Protocol, Copyright © 2003 Eric Glass -// http://davenport.sourceforge.net/ntlm.html -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// - -using System; -using System.Text; - -#if NETFX_CORE -using Encoding = Portable.Text.Encoding; -using MD5 = MimeKit.Cryptography.MD5; -#else -using System.Security.Cryptography; -#endif - -namespace MailKit.Security.Ntlm { - static class ChallengeResponse2 - { - static readonly byte[] Magic = { 0x4B, 0x47, 0x53, 0x21, 0x40, 0x23, 0x24, 0x25 }; - - // This is the pre-encrypted magic value with a null DES key (0xAAD3B435B51404EE) - // Ref: http://packetstormsecurity.nl/Crackers/NT/l0phtcrack/l0phtcrack2.5-readme.html - static readonly byte[] NullEncMagic = { 0xAA, 0xD3, 0xB4, 0x35, 0xB5, 0x14, 0x04, 0xEE }; - - static byte[] ComputeLM (string password, byte[] challenge) - { - var buffer = new byte[21]; - - // create Lan Manager password - using (var des = DES.Create ()) { - des.Mode = CipherMode.ECB; - - // Note: In .NET DES cannot accept a weak key - // this can happen for a null password - if (string.IsNullOrEmpty (password)) { - Buffer.BlockCopy (NullEncMagic, 0, buffer, 0, 8); - } else { - des.Key = PasswordToKey (password, 0); - using (var ct = des.CreateEncryptor ()) - ct.TransformBlock (Magic, 0, 8, buffer, 0); - } - - // and if a password has less than 8 characters - if (password == null || password.Length < 8) { - Buffer.BlockCopy (NullEncMagic, 0, buffer, 8, 8); - } else { - des.Key = PasswordToKey (password, 7); - using (var ct = des.CreateEncryptor ()) - ct.TransformBlock (Magic, 0, 8, buffer, 8); - } - } - - return GetResponse (challenge, buffer); - } - - static byte[] ComputeNtlmPassword (string password) - { - var buffer = new byte[21]; - - // create NT password - using (var md4 = new MD4 ()) { - var data = password == null ? new byte[0] : Encoding.Unicode.GetBytes (password); - var hash = md4.ComputeHash (data); - Buffer.BlockCopy (hash, 0, buffer, 0, 16); - - // clean up - Array.Clear (data, 0, data.Length); - Array.Clear (hash, 0, hash.Length); - } - - return buffer; - } - - static byte[] ComputeNtlm (string password, byte[] challenge) - { - var buffer = ComputeNtlmPassword (password); - return GetResponse (challenge, buffer); - } - - static void ComputeNtlmV2Session (string password, byte[] challenge, out byte[] lm, out byte[] ntlm) - { - var nonce = new byte[8]; - - using (var rng = RandomNumberGenerator.Create ()) - rng.GetBytes (nonce); - - var sessionNonce = new byte[challenge.Length + 8]; - challenge.CopyTo (sessionNonce, 0); - nonce.CopyTo (sessionNonce, challenge.Length); - - lm = new byte[24]; - nonce.CopyTo (lm, 0); - - using (var md5 = MD5.Create ()) { - var hash = md5.ComputeHash (sessionNonce); - var newChallenge = new byte[8]; - - Array.Copy (hash, newChallenge, 8); - - ntlm = ComputeNtlm (password, newChallenge); - - // clean up - Array.Clear (newChallenge, 0, newChallenge.Length); - Array.Clear (hash, 0, hash.Length); - } - - // clean up - Array.Clear (sessionNonce, 0, sessionNonce.Length); - Array.Clear (nonce, 0, nonce.Length); - } - - static byte[] ComputeNtlmV2 (Type2Message type2, string username, string password, string domain) - { - var ntlm_hash = ComputeNtlmPassword (password); - - var ubytes = Encoding.Unicode.GetBytes (username.ToUpperInvariant ()); - var tbytes = Encoding.Unicode.GetBytes (domain); - - var bytes = new byte[ubytes.Length + tbytes.Length]; - ubytes.CopyTo (bytes, 0); - Array.Copy (tbytes, 0, bytes, ubytes.Length, tbytes.Length); - - byte[] ntlm_v2_hash; - - using (var md5 = new HMACMD5 (ntlm_hash)) - ntlm_v2_hash = md5.ComputeHash (bytes); - - Array.Clear (ntlm_hash, 0, ntlm_hash.Length); - - using (var md5 = new HMACMD5 (ntlm_v2_hash)) { - var now = DateTime.Now; - var timestamp = now.Ticks - 504911232000000000; - var nonce = new byte[8]; - - using (var rng = RandomNumberGenerator.Create ()) - rng.GetBytes (nonce); - - var targetInfo = type2.EncodedTargetInfo; - var blob = new byte[28 + targetInfo.Length]; - blob[0] = 0x01; - blob[1] = 0x01; - - Buffer.BlockCopy (BitConverterLE.GetBytes (timestamp), 0, blob, 8, 8); - - Buffer.BlockCopy (nonce, 0, blob, 16, 8); - Buffer.BlockCopy (targetInfo, 0, blob, 28, targetInfo.Length); - - var challenge = type2.Nonce; - - var hashInput = new byte[challenge.Length + blob.Length]; - challenge.CopyTo (hashInput, 0); - blob.CopyTo (hashInput, challenge.Length); - - var blobHash = md5.ComputeHash (hashInput); - - var response = new byte[blob.Length + blobHash.Length]; - blobHash.CopyTo (response, 0); - blob.CopyTo (response, blobHash.Length); - - Array.Clear (ntlm_v2_hash, 0, ntlm_v2_hash.Length); - Array.Clear (hashInput, 0, hashInput.Length); - Array.Clear (blobHash, 0, blobHash.Length); - Array.Clear (nonce, 0, nonce.Length); - Array.Clear (blob, 0, blob.Length); - - return response; - } - } - - public static void Compute (Type2Message type2, NtlmAuthLevel level, string username, string password, string domain, out byte[] lm, out byte[] ntlm) - { - lm = null; - - switch (level) { - case NtlmAuthLevel.LM_and_NTLM: - lm = ComputeLM (password, type2.Nonce); - ntlm = ComputeNtlm (password, type2.Nonce); - break; - case NtlmAuthLevel.LM_and_NTLM_and_try_NTLMv2_Session: - if ((type2.Flags & NtlmFlags.NegotiateNtlm2Key) == 0) - goto case NtlmAuthLevel.LM_and_NTLM; - ComputeNtlmV2Session (password, type2.Nonce, out lm, out ntlm); - break; - case NtlmAuthLevel.NTLM_only: - if ((type2.Flags & NtlmFlags.NegotiateNtlm2Key) != 0) - ComputeNtlmV2Session (password, type2.Nonce, out lm, out ntlm); - else - ntlm = ComputeNtlm (password, type2.Nonce); - break; - case NtlmAuthLevel.NTLMv2_only: - ntlm = ComputeNtlmV2 (type2, username, password, domain); - break; - default: - throw new InvalidOperationException (); - } - } - - static byte[] GetResponse (byte[] challenge, byte[] pwd) - { - var response = new byte[24]; - - using (var des = DES.Create ()) { - des.Mode = CipherMode.ECB; - des.Key = PrepareDESKey (pwd, 0); - - using (var ct = des.CreateEncryptor ()) - ct.TransformBlock (challenge, 0, 8, response, 0); - - des.Key = PrepareDESKey (pwd, 7); - - using (var ct = des.CreateEncryptor ()) - ct.TransformBlock (challenge, 0, 8, response, 8); - - des.Key = PrepareDESKey (pwd, 14); - - using (var ct = des.CreateEncryptor ()) - ct.TransformBlock (challenge, 0, 8, response, 16); - } - - return response; - } - - static byte[] PrepareDESKey (byte[] key56bits, int position) - { - // convert to 8 bytes - var key = new byte[8]; - - key[0] = key56bits [position]; - key[1] = (byte) ((key56bits[position] << 7) | (key56bits[position + 1] >> 1)); - key[2] = (byte) ((key56bits[position + 1] << 6) | (key56bits[position + 2] >> 2)); - key[3] = (byte) ((key56bits[position + 2] << 5) | (key56bits[position + 3] >> 3)); - key[4] = (byte) ((key56bits[position + 3] << 4) | (key56bits[position + 4] >> 4)); - key[5] = (byte) ((key56bits[position + 4] << 3) | (key56bits[position + 5] >> 5)); - key[6] = (byte) ((key56bits[position + 5] << 2) | (key56bits[position + 6] >> 6)); - key[7] = (byte) (key56bits[position + 6] << 1); - - return key; - } - - static byte[] PasswordToKey (string password, int position) - { - int len = Math.Min (password.Length - position, 7); - var key7 = new byte[7]; - - Encoding.ASCII.GetBytes (password.ToUpper (), position, len, key7, 0); - var key8 = PrepareDESKey (key7, 0); - - // cleanup intermediate key material - Array.Clear (key7, 0, key7.Length); - - return key8; - } - } -} diff --git a/MailKit/Security/Ntlm/DES.cs b/MailKit/Security/Ntlm/DES.cs index b827a46467..9297fbba3f 100644 --- a/MailKit/Security/Ntlm/DES.cs +++ b/MailKit/Security/Ntlm/DES.cs @@ -3,7 +3,7 @@ // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2017 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -25,143 +25,12 @@ // using System; - -#if NETFX_CORE -using Windows.Storage.Streams; -using Windows.Security.Cryptography; -using Windows.Security.Cryptography.Core; -#else using System.Security.Cryptography; -using Org.BouncyCastle.Crypto; using Org.BouncyCastle.Crypto.Engines; using Org.BouncyCastle.Crypto.Parameters; -#endif namespace MailKit.Security.Ntlm { -#if NETFX_CORE - enum CipherMode { - CBC, - ECB - } - - sealed class SymmetricKeyEncryptor : IDisposable - { - readonly SymmetricKeyAlgorithmProvider algorithm; - readonly CryptographicKey key; - readonly IBuffer iv; - - public SymmetricKeyEncryptor (SymmetricKeyAlgorithmProvider algorithm, CryptographicKey key, IBuffer iv) - { - this.algorithm = algorithm; - this.key = key; - this.iv = iv; - } - - public int TransformBlock (byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset) - { - if (inputBuffer == null) - throw new ArgumentNullException ("inputBuffer"); - - if (inputOffset < 0 || inputOffset > inputBuffer.Length) - throw new ArgumentOutOfRangeException ("inputOffset"); - - if (inputCount < 0 || inputOffset > inputBuffer.Length - inputCount) - throw new ArgumentOutOfRangeException ("inputCount"); - - if (inputCount != 8) - throw new ArgumentOutOfRangeException ("inputCount", "Can only transform 8 bytes at a time."); - - if (outputBuffer == null) - throw new ArgumentNullException ("outputBuffer"); - - if (outputOffset < 0 || outputOffset > outputBuffer.Length - algorithm.BlockLength) - throw new ArgumentOutOfRangeException ("outputOffset"); - - IBuffer encrypted, data; - byte[] input, output; - - input = new byte[inputCount]; - Array.Copy (inputBuffer, inputOffset, input, 0, inputCount); - data = CryptographicBuffer.CreateFromByteArray (input); - - encrypted = CryptographicEngine.Encrypt (key, data, iv); - - CryptographicBuffer.CopyToByteArray (encrypted, out output); - - Array.Copy (output, 0, outputBuffer, outputOffset, output.Length); - - return output.Length; - } - - public void Dispose () - { - } - } - - sealed class DES : IDisposable - { - public static DES Create () - { - return new DES (); - } - - ~DES () - { - Dispose (false); - } - - public CipherMode Mode { - get; set; - } - - public byte[] Key { - get; set; - } - - public void Clear () - { - Dispose (); - } - - public SymmetricKeyEncryptor CreateEncryptor () - { - var buffer = CryptographicBuffer.CreateFromByteArray (Key); - SymmetricKeyAlgorithmProvider algorithm; - CryptographicKey key; - string algorithmName; - IBuffer iv = null; - - switch (Mode) { - case CipherMode.CBC: algorithmName = SymmetricAlgorithmNames.DesCbc; break; - case CipherMode.ECB: algorithmName = SymmetricAlgorithmNames.DesEcb; break; - default: throw new IndexOutOfRangeException ("Mode"); - } - - algorithm = SymmetricKeyAlgorithmProvider.OpenAlgorithm (algorithmName); - key = algorithm.CreateSymmetricKey (buffer); - - if (Mode == CipherMode.CBC) - iv = CryptographicBuffer.GenerateRandom (algorithm.BlockLength); - - return new SymmetricKeyEncryptor (algorithm, key, iv); - } - - void Dispose (bool disposing) - { - if (Key != null) { - Array.Clear (Key, 0, Key.Length); - Key = null; - } - } - - public void Dispose () - { - Dispose (true); - GC.SuppressFinalize (this); - } - } -#else class DES : SymmetricAlgorithm { DES () @@ -360,5 +229,4 @@ static ulong QuadWordFromBigEndian (byte[] block) (((ulong) block[6]) << 8) | ((ulong) block[7]); } } -#endif } diff --git a/MailKit/Security/Ntlm/HMACMD5.cs b/MailKit/Security/Ntlm/HMACMD5.cs index 9de82244b1..ea88e8e91e 100644 --- a/MailKit/Security/Ntlm/HMACMD5.cs +++ b/MailKit/Security/Ntlm/HMACMD5.cs @@ -3,7 +3,7 @@ // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2017 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -27,197 +27,11 @@ using System; using System.IO; -#if NETFX_CORE -using Windows.Security.Cryptography; -using Windows.Security.Cryptography.Core; -#else -using System.Security.Cryptography; - -using Org.BouncyCastle.Crypto; using Org.BouncyCastle.Crypto.Macs; using Org.BouncyCastle.Crypto.Digests; using Org.BouncyCastle.Crypto.Parameters; -#endif - -namespace MailKit.Security.Ntlm -{ -#if NETFX_CORE - class HMACMD5 : IDisposable - { - MacAlgorithmProvider algorithm; - CryptographicHash hash; - byte[] hashValue, key; - bool disposed; - - public HMACMD5 (byte[] key) - { - Key = key; - } - - ~HMACMD5 () - { - Dispose (false); - } - - public byte[] Hash { - get { - if (hashValue == null) - throw new InvalidOperationException ("No hash value computed."); - - return hashValue; - } - } - - public byte[] Key { - get { return key; } - set { - if (value == null) - throw new ArgumentNullException (nameof (value)); - - if (key != null) - Array.Clear (key, 0, key.Length); - - key = value; - Initialize (); - } - } - - void HashCore (byte[] block, int offset, int size) - { - var array = new byte[size]; - Buffer.BlockCopy (block, offset, array, 0, size); - - var buffer = CryptographicBuffer.CreateFromByteArray (array); - hash.Append (buffer); - } - - byte[] HashFinal () - { - var buffer = hash.GetValueAndReset (); - byte[] value; - - CryptographicBuffer.CopyToByteArray (buffer, out value); - - return value; - } - - public void Initialize () - { - var buffer = CryptographicBuffer.CreateFromByteArray (Key); - - algorithm = MacAlgorithmProvider.OpenAlgorithm (MacAlgorithmNames.HmacMd5); - hash = algorithm.CreateHash (buffer); - } - - public void Clear () - { - Dispose (false); - } - - public byte[] ComputeHash (byte[] buffer, int offset, int count) - { - if (buffer == null) - throw new ArgumentNullException (nameof (buffer)); - - if (offset < 0 || offset > buffer.Length) - throw new ArgumentOutOfRangeException (nameof (offset)); - - if (count < 0 || offset > buffer.Length - count) - throw new ArgumentOutOfRangeException (nameof (count)); - - if (disposed) - throw new ObjectDisposedException ("HashAlgorithm"); - - HashCore (buffer, offset, count); - hashValue = HashFinal (); - - return hashValue; - } - - public byte[] ComputeHash (byte[] buffer) - { - if (buffer == null) - throw new ArgumentNullException (nameof (buffer)); - - return ComputeHash (buffer, 0, buffer.Length); - } - - public byte[] ComputeHash (Stream inputStream) - { - // don't read stream unless object is ready to use - if (disposed) - throw new ObjectDisposedException ("HashAlgorithm"); - - var buffer = new byte[4096]; - int nread; - - do { - if ((nread = inputStream.Read (buffer, 0, buffer.Length)) > 0) - HashCore (buffer, 0, nread); - } while (nread > 0); - - hashValue = HashFinal (); - - return hashValue; - } - - public int TransformBlock (byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset) - { - if (inputBuffer == null) - throw new ArgumentNullException (nameof (inputBuffer)); - - if (inputOffset < 0 || inputOffset > inputBuffer.Length) - throw new ArgumentOutOfRangeException (nameof (inputOffset)); - - if (inputCount < 0 || inputOffset > inputBuffer.Length - inputCount) - throw new ArgumentOutOfRangeException (nameof (inputCount)); - - if (outputBuffer != null) { - if (outputOffset < 0 || outputOffset > outputBuffer.Length - inputCount) - throw new ArgumentOutOfRangeException (nameof (outputOffset)); - } - - HashCore (inputBuffer, inputOffset, inputCount); - - if (outputBuffer != null) - Buffer.BlockCopy (inputBuffer, inputOffset, outputBuffer, outputOffset, inputCount); - - return inputCount; - } - - public byte[] TransformFinalBlock (byte[] inputBuffer, int inputOffset, int inputCount) - { - if (inputCount < 0) - throw new ArgumentOutOfRangeException (nameof (inputCount)); - - var outputBuffer = new byte[inputCount]; - - // note: other exceptions are handled by Buffer.BlockCopy - Buffer.BlockCopy (inputBuffer, inputOffset, outputBuffer, 0, inputCount); - - HashCore (inputBuffer, inputOffset, inputCount); - hashValue = HashFinal (); - - return outputBuffer; - } - - void Dispose (bool disposing) - { - if (key != null) { - Array.Clear (key, 0, Key.Length); - key = null; - } - } - - public void Dispose () - { - Dispose (true); - GC.SuppressFinalize (this); - disposed = true; - } - } -#else +namespace MailKit.Security.Ntlm { class HMACMD5 : IDisposable { readonly HMac hash = new HMac (new MD5Digest ()); @@ -385,5 +199,4 @@ public void Dispose () disposed = true; } } -#endif } diff --git a/MailKit/Security/Ntlm/MD4.cs b/MailKit/Security/Ntlm/MD4.cs index c621e411dd..7ae9aaddaf 100644 --- a/MailKit/Security/Ntlm/MD4.cs +++ b/MailKit/Security/Ntlm/MD4.cs @@ -1,4 +1,4 @@ -// +// // MD4.cs // // Authors: Sebastien Pouliot @@ -6,7 +6,7 @@ // // Copyright (c) 2003 Motus Technologies Inc. (http://www.motus.com) // Copyright (c) 2004-2005, 2010 Novell, Inc (http://www.novell.com) -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining // a copy of this software and associated documentation files (the @@ -47,12 +47,12 @@ sealed class MD4 : IDisposable const int S33 = 11; const int S34 = 15; + readonly byte[] buffered; + readonly uint[] state; + readonly uint[] count; + readonly uint[] x; + byte[]? hashValue; bool disposed; - byte[] hashValue; - byte[] buffered; - uint[] state; - uint[] count; - uint[] x; public MD4 () { @@ -155,7 +155,7 @@ static byte[] Padding (int length) return padding; } - return null; + return Array.Empty (); } // F, G and H are basic MD4 functions. @@ -315,6 +315,9 @@ public byte[] ComputeHash (byte[] buffer) public byte[] ComputeHash (Stream inputStream) { + if (inputStream == null) + throw new ArgumentNullException (nameof (inputStream)); + // don't read stream unless object is ready to use if (disposed) throw new ObjectDisposedException (nameof (MD4)); @@ -376,24 +379,12 @@ public byte[] TransformFinalBlock (byte[] inputBuffer, int inputOffset, int inpu void Dispose (bool disposing) { - if (buffered != null) { + if (disposing && !disposed) { Array.Clear (buffered, 0, buffered.Length); - buffered = null; - } - - if (state != null) { Array.Clear (state, 0, state.Length); - state = null; - } - - if (count != null) { Array.Clear (count, 0, count.Length); - count = null; - } - - if (x != null) { Array.Clear (x, 0, x.Length); - x = null; + disposed = true; } } @@ -401,7 +392,6 @@ public void Dispose () { Dispose (true); GC.SuppressFinalize (this); - disposed = true; } } } diff --git a/MailKit/Security/Ntlm/MessageBase.cs b/MailKit/Security/Ntlm/MessageBase.cs deleted file mode 100644 index e7d33a1641..0000000000 --- a/MailKit/Security/Ntlm/MessageBase.cs +++ /dev/null @@ -1,100 +0,0 @@ -// -// Mono.Security.Protocol.Ntlm.MessageBase -// abstract class for all NTLM messages -// -// Author: -// Sebastien Pouliot -// -// Copyright (C) 2003 Motus Technologies Inc. (http://www.motus.com) -// Copyright (C) 2004 Novell, Inc (http://www.novell.com) -// -// References -// a. NTLM Authentication Scheme for HTTP, Ronald Tschalär -// http://www.innovation.ch/java/ntlm.html -// b. The NTLM Authentication Protocol, Copyright © 2003 Eric Glass -// http://davenport.sourceforge.net/ntlm.html -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// - -using System; - -namespace MailKit.Security.Ntlm { - abstract class MessageBase - { - static readonly byte[] header = { 0x4e, 0x54, 0x4c, 0x4d, 0x53, 0x53, 0x50, 0x00 }; - - readonly int type; - - protected MessageBase (int messageType) - { - type = messageType; - } - - public NtlmFlags Flags { - get; set; - } - - public int Type { - get { return type; } - } - - protected byte[] PrepareMessage (int size) - { - var message = new byte[size]; - - Buffer.BlockCopy (header, 0, message, 0, 8); - - message[ 8] = (byte) type; - message[ 9] = (byte)(type >> 8); - message[10] = (byte)(type >> 16); - message[11] = (byte)(type >> 24); - - return message; - } - - bool CheckHeader (byte[] message, int startIndex) - { - for (int i = 0; i < header.Length; i++) { - if (message[startIndex + i] != header[i]) - return false; - } - - return BitConverterLE.ToUInt32 (message, startIndex + 8) == type; - } - - protected void ValidateArguments (byte[] message, int startIndex, int length) - { - if (message == null) - throw new ArgumentNullException (nameof (message)); - - if (startIndex < 0 || startIndex > message.Length) - throw new ArgumentOutOfRangeException (nameof (startIndex)); - - if (length < 12 || length > (message.Length - startIndex)) - throw new ArgumentOutOfRangeException (nameof (length)); - - if (!CheckHeader (message, startIndex)) - throw new ArgumentException (string.Format ("Invalid Type{0} message.", type), nameof (message)); - } - - public abstract byte[] Encode (); - } -} diff --git a/MailKit/Security/Ntlm/NtlmAttribute.cs b/MailKit/Security/Ntlm/NtlmAttribute.cs new file mode 100644 index 0000000000..c98504fba2 --- /dev/null +++ b/MailKit/Security/Ntlm/NtlmAttribute.cs @@ -0,0 +1,45 @@ +// +// NtlmAttribute.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/b38c36ed-2804-4868-a9ff-8dd3182128e4 + +namespace MailKit.Security.Ntlm +{ + enum NtlmAttribute : short + { + EOL = 0, + ServerName = 1, + DomainName = 2, + DnsServerName = 3, + DnsDomainName = 4, + DnsTreeName = 5, + Flags = 6, + Timestamp = 7, + SingleHost = 8, + TargetName = 9, + ChannelBinding = 10 + } +} diff --git a/MailKit/Security/Ntlm/NtlmAttributeValuePair.cs b/MailKit/Security/Ntlm/NtlmAttributeValuePair.cs new file mode 100644 index 0000000000..cfbb5a6a65 --- /dev/null +++ b/MailKit/Security/Ntlm/NtlmAttributeValuePair.cs @@ -0,0 +1,418 @@ +// +// NtlmAttributeValuePair.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/b38c36ed-2804-4868-a9ff-8dd3182128e4 + +using System; +using System.Text; +using System.Diagnostics; + +namespace MailKit.Security.Ntlm { + /// + /// An abstract NTLM attribute and value pair. + /// + /// + /// An abstract NTLM attribute and value pair. + /// + abstract class NtlmAttributeValuePair + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new NTLM attribute and value pair. + /// + /// The NTLM attribute. + protected NtlmAttributeValuePair (NtlmAttribute attr) + { + Attribute = attr; + } + + /// + /// Get the NTLM attribute that this pair represents. + /// + /// + /// Gets the NTLM attribute that this pair represents. + /// + /// The NTLM attribute. + public NtlmAttribute Attribute { + get; private set; + } + + protected static void EncodeInt16 (byte[] buf, ref int index, short value) + { + buf[index++] = (byte) (value); + buf[index++] = (byte) (value >> 8); + } + + protected static void EncodeInt32 (byte[] buf, ref int index, int value) + { + buf[index++] = (byte) (value); + buf[index++] = (byte) (value >> 8); + buf[index++] = (byte) (value >> 16); + buf[index++] = (byte) (value >> 24); + } + + protected static void EncodeTypeAndLength (byte[] buf, ref int index, NtlmAttribute attr, short length) + { + EncodeInt16 (buf, ref index, (short) attr); + EncodeInt16 (buf, ref index, length); + } + + /// + /// Get the number of bytes needed for encoding the attribute value. + /// + /// + /// Gets the number of bytes needed for encoding the attribute value. + /// + /// The text encoding. + /// The number of bytes needed to encode the value. + public abstract int GetEncodedLength (Encoding encoding); + + /// + /// Encode the attribute value to the specified buffer. + /// + /// + /// Encodes the attribute value to the specified buffer. + /// + /// The text encoding. + /// The output buffer. + /// The index into the buffer to start appending the encoded attribute. + public abstract void EncodeTo (Encoding encoding, byte[] buffer, ref int index); + } + + /// + /// An NTLM attribute and value pair consisting of a string value. + /// + /// + /// An NTLM attribute and value pair consisting of a string value. + /// + [DebuggerDisplay ("{Attribute} = {Value}")] + sealed class NtlmAttributeStringValuePair : NtlmAttributeValuePair + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new NTLM attribute and value pair consisting of a string value. + /// + /// The NTLM attribute. + /// The NTLM attribute value. + public NtlmAttributeStringValuePair (NtlmAttribute attr, string value) : base (attr) + { + Value = value; + } + + /// + /// Get or set the value of the attribute. + /// + /// + /// Gets or sets the value of the attribute. + /// + /// The attribute value. + public string Value { + get; set; + } + + /// + /// Get the number of bytes needed for encoding the attribute value. + /// + /// + /// Gets the number of bytes needed for encoding the attribute value. + /// + /// The text encoding. + /// The number of bytes needed to encode the value. + public override int GetEncodedLength (Encoding encoding) + { + return 4 + encoding.GetByteCount (Value); + } + + /// + /// Encode the attribute value to the specified buffer. + /// + /// + /// Encodes the attribute value to the specified buffer. + /// + /// The text encoding. + /// The output buffer. + /// The index into the buffer to start appending the encoded attribute. + public override void EncodeTo (Encoding encoding, byte[] buffer, ref int index) + { + int length = encoding.GetByteCount (Value); + + EncodeTypeAndLength (buffer, ref index, Attribute, (short) length); + encoding.GetBytes (Value, 0, Value.Length, buffer, index); + index += length; + } + } + + /// + /// An NTLM attribute and value pair consisting of a flags value. + /// + /// + /// An NTLM attribute and value pair consisting of a flags value. + /// + [DebuggerDisplay ("{Attribute} = {Value}")] + sealed class NtlmAttributeFlagsValuePair : NtlmAttributeValuePair + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new NTLM attribute and value pair consisting of a flags value. + /// + /// The NTLM attribute. + /// The NTLM attribute value. + /// The size of the encoded flags value. + internal NtlmAttributeFlagsValuePair (NtlmAttribute attr, int value, short size) : base (attr) + { + Value = value; + Size = size; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new NTLM attribute and value pair consisting of a flags value. + /// + /// The NTLM attribute. + /// The NTLM attribute value. + public NtlmAttributeFlagsValuePair (NtlmAttribute attr, int value) : this (attr, value, 4) + { + } + + /// + /// Get or set the size of the encoded flags value. + /// + /// + /// Gets or sets the size of the encoded flags value. + /// + public short Size { + get; internal set; + } + + /// + /// Get or set the value of the attribute. + /// + /// + /// Gets or sets the value of the attribute. + /// + /// The attribute value. + public int Value { + get; set; + } + + /// + /// Get the number of bytes needed for encoding the attribute value. + /// + /// + /// Gets the number of bytes needed for encoding the attribute value. + /// + /// The text encoding. + /// The number of bytes needed to encode the value. + public override int GetEncodedLength (Encoding encoding) + { + return 4 + Size; + } + + /// + /// Encode the attribute value to the specified buffer. + /// + /// + /// Encodes the attribute value to the specified buffer. + /// + /// The text encoding. + /// The output buffer. + /// The index into the buffer to start appending the encoded attribute. + public override void EncodeTo (Encoding encoding, byte[] buffer, ref int index) + { + EncodeTypeAndLength (buffer, ref index, Attribute, Size); + + switch (Size) { + case 2: EncodeInt16 (buffer, ref index, (short) Value); break; + default: EncodeInt32 (buffer, ref index, Value); break; + } + } + } + + /// + /// An NTLM attribute and value pair consisting of a timestamp value. + /// + /// + /// An NTLM attribute and value pair consisting of a timestamp value. + /// + [DebuggerDisplay ("{Attribute} = {Value}")] + sealed class NtlmAttributeTimestampValuePair : NtlmAttributeValuePair + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new NTLM attribute and value pair consisting of a timestamp value. + /// + /// The NTLM attribute. + /// The NTLM attribute value. + /// The size of the encoded flags value. + internal NtlmAttributeTimestampValuePair (NtlmAttribute attr, long value, short size) : base (attr) + { + Value = value; + Size = size; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new NTLM attribute and value pair consisting of a timestamp value. + /// + /// The NTLM attribute. + /// The NTLM attribute value. + public NtlmAttributeTimestampValuePair (NtlmAttribute attr, long value) : this (attr, value, 8) + { + } + + /// + /// Get or set the size of the encoded timestamp value. + /// + /// + /// Gets or sets the size of the encoded timestamp value. + /// + public short Size { + get; internal set; + } + + /// + /// Get or set the value of the attribute. + /// + /// + /// Gets or sets the value of the attribute. + /// + /// The attribute value. + public long Value { + get; set; + } + + /// + /// Get the number of bytes needed for encoding the attribute value. + /// + /// + /// Gets the number of bytes needed for encoding the attribute value. + /// + /// The text encoding. + /// The number of bytes needed to encode the value. + public override int GetEncodedLength (Encoding encoding) + { + return 4 + Size; + } + + /// + /// Encode the attribute value to the specified buffer. + /// + /// + /// Encodes the attribute value to the specified buffer. + /// + /// The text encoding. + /// The output buffer. + /// The index into the buffer to start appending the encoded attribute. + public override void EncodeTo (Encoding encoding, byte[] buffer, ref int index) + { + EncodeTypeAndLength (buffer, ref index, Attribute, Size); + + switch (Size) { + case 2: EncodeInt16 (buffer, ref index, (short) (Value & 0xffff)); break; + case 4: EncodeInt32 (buffer, ref index, (int) (Value & 0xffffffff)); break; + default: + EncodeInt32 (buffer, ref index, (int) (Value & 0xffffffff)); + EncodeInt32 (buffer, ref index, (int) (Value >> 32)); + break; + } + } + } + + /// + /// An NTLM attribute and value pair consisting of a byte array value. + /// + /// + /// An NTLM attribute and value pair consisting of a byte array value. + /// + sealed class NtlmAttributeByteArrayValuePair : NtlmAttributeValuePair + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new NTLM attribute and value pair consisting of a byte array value. + /// + /// The NTLM attribute. + /// The NTLM attribute value. + public NtlmAttributeByteArrayValuePair (NtlmAttribute attr, byte[] value) : base (attr) + { + Value = value; + } + + /// + /// Get or set the value of the attribute. + /// + /// + /// Gets or sets the value of the attribute. + /// + /// The attribute value. + public byte[] Value { + get; set; + } + + /// + /// Get the number of bytes needed for encoding the attribute value. + /// + /// + /// Gets the number of bytes needed for encoding the attribute value. + /// + /// The text encoding. + /// The number of bytes needed to encode the value. + public override int GetEncodedLength (Encoding encoding) + { + return 4 + Value.Length; + } + + /// + /// Encode the attribute value to the specified buffer. + /// + /// + /// Encodes the attribute value to the specified buffer. + /// + /// The text encoding. + /// The output buffer. + /// The index into the buffer to start appending the encoded attribute. + public override void EncodeTo (Encoding encoding, byte[] buffer, ref int index) + { + EncodeTypeAndLength (buffer, ref index, Attribute, (short) Value.Length); + + Buffer.BlockCopy (Value, 0, buffer, index, Value.Length); + index += Value.Length; + } + } +} diff --git a/MailKit/Security/Ntlm/NtlmAuthenticateMessage.cs b/MailKit/Security/Ntlm/NtlmAuthenticateMessage.cs new file mode 100644 index 0000000000..ff3a6ef295 --- /dev/null +++ b/MailKit/Security/Ntlm/NtlmAuthenticateMessage.cs @@ -0,0 +1,465 @@ +// +// NtlmAuthenticateMessage.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/b38c36ed-2804-4868-a9ff-8dd3182128e4 + +using System; +using System.Text; +using System.Diagnostics.CodeAnalysis; + +namespace MailKit.Security.Ntlm { + class NtlmAuthenticateMessage : NtlmMessageBase + { + static readonly byte[] Z16 = new byte[16]; + + readonly NtlmNegotiateMessage? negotiate; + readonly NtlmChallengeMessage? challenge; + byte[] clientChallenge; + + public NtlmAuthenticateMessage (NtlmNegotiateMessage negotiate, NtlmChallengeMessage challenge, string userName, string password, string domain, string workstation) : base (3) + { + if (negotiate == null) + throw new ArgumentNullException (nameof (negotiate)); + + if (challenge == null) + throw new ArgumentNullException (nameof (challenge)); + + if (userName == null) + throw new ArgumentNullException (nameof (userName)); + + if (password == null) + throw new ArgumentNullException (nameof (password)); + + clientChallenge = NtlmUtils.NONCE (8); + this.negotiate = negotiate; + this.challenge = challenge; + + if (!string.IsNullOrEmpty (domain)) { + Domain = domain; + } else if ((challenge.Flags & NtlmFlags.TargetTypeDomain) != 0) { + // The server is domain-joined, so the TargetName will be the domain. + Domain = challenge.TargetName ?? string.Empty; + } else if (challenge.TargetInfo != null) { + // The server is not domain-joined, so the TargetName will be the machine name of the server. + Domain = challenge.TargetInfo.DomainName ?? string.Empty; + } else { + Domain = string.Empty; + } + + Workstation = workstation; + UserName = userName; + Password = password; + + // Use only the features supported by both the client and server. + Flags = negotiate.Flags & challenge.Flags; + + // If the client and server both support NEGOTIATE_UNICODE, disable NEGOTIATE_OEM. + if ((Flags & NtlmFlags.NegotiateUnicode) != 0) + Flags &= ~NtlmFlags.NegotiateOem; + // TODO: throw if Unicode && Oem are both unset? + + // If the client and server both support NEGOTIATE_EXTENDED_SESSIONSECURITY, disable NEGOTIATE_LM_KEY. + if ((Flags & NtlmFlags.NegotiateExtendedSessionSecurity) != 0) + Flags &= ~NtlmFlags.NegotiateLanManagerKey; + + // Disable NEGOTIATE_KEY_EXCHANGE if neither NEGOTIATE_SIGN nor NEGOTIATE_SEAL are also present. + if ((Flags & NtlmFlags.NegotiateKeyExchange) != 0 && (Flags & (NtlmFlags.NegotiateSign | NtlmFlags.NegotiateSeal)) == 0) + Flags &= ~NtlmFlags.NegotiateKeyExchange; + + // If we had RequestTarget in our initial NEGOTIATE_MESSAGE, include it again in this message(?) + if ((negotiate.Flags & NtlmFlags.RequestTarget) != 0) + Flags |= NtlmFlags.RequestTarget; + + // If NEGOTIATE_VERSION is set, grab the OSVersion from our original negotiate message. + if ((Flags & NtlmFlags.NegotiateVersion) != 0) + OSVersion = negotiate.OSVersion ?? OSVersion; + } + + // Note: This .ctor is for debugging purposes only. It allows us to decode an NTLM AUTHENTICATE_MESSAGE without having to go through the entire NTLM authentication process. + public NtlmAuthenticateMessage (byte[] message, int startIndex, int length) : base (3) + { + Decode (message, startIndex, length); + clientChallenge = Array.Empty (); + challenge = null; + Password = string.Empty; + } + + ~NtlmAuthenticateMessage () + { + if (clientChallenge != null && clientChallenge.Length > 0) + Array.Clear (clientChallenge, 0, clientChallenge.Length); + + if (LmChallengeResponse != null) + Array.Clear (LmChallengeResponse, 0, LmChallengeResponse.Length); + + if (NtChallengeResponse != null) + Array.Clear (NtChallengeResponse, 0, NtChallengeResponse.Length); + + if (ExportedSessionKey != null) + Array.Clear (ExportedSessionKey, 0, ExportedSessionKey.Length); + + if (EncryptedRandomSessionKey != null) + Array.Clear (EncryptedRandomSessionKey, 0, EncryptedRandomSessionKey.Length); + } + + /// + /// This is only used for unit testing purposes. + /// + internal byte[]? ClientChallenge { + get { return clientChallenge; } + set { + if (value == null) + return; + + if (value.Length != 8) + throw new ArgumentException ("Invalid nonce length (should be 8 bytes).", nameof (value)); + + Array.Clear (clientChallenge, 0, clientChallenge.Length); + clientChallenge = value; + } + } + + /// + /// This is only used for unit testing purposes. + /// + internal long? Timestamp { + get; set; + } + + public string Domain { + get; private set; + } + + public string Workstation { + get; private set; + } + + public string Password { + get; private set; + } + + public string UserName { + get; private set; + } + + public byte[]? Mic { + get; private set; + } + + public byte[]? LmChallengeResponse { + get; private set; + } + + public byte[]? NtChallengeResponse { + get; private set; + } + + public byte[]? ExportedSessionKey { + get; private set; + } + + public byte[]? EncryptedRandomSessionKey { + get; private set; + } + + [MemberNotNull (nameof (LmChallengeResponse), nameof (NtChallengeResponse), nameof (Domain), nameof (UserName), nameof (Workstation), nameof (EncryptedRandomSessionKey))] + void Decode (byte[] message, int startIndex, int length) + { + int payloadOffset = length; + int micOffset = 64; + + ValidateArguments (message, startIndex, length); + + if (message.Length >= 64) + Flags = (NtlmFlags) BitConverterLE.ToUInt32 (message, startIndex + 60); + else + Flags = (NtlmFlags) 0x8201; + + int lmLength = BitConverterLE.ToUInt16 (message, startIndex + 12); + int lmOffset = BitConverterLE.ToUInt16 (message, startIndex + 16); + LmChallengeResponse = new byte[lmLength]; + Buffer.BlockCopy (message, startIndex + lmOffset, LmChallengeResponse, 0, lmLength); + payloadOffset = Math.Min (payloadOffset, lmOffset); + + int ntLength = BitConverterLE.ToUInt16 (message, startIndex + 20); + int ntOffset = BitConverterLE.ToUInt16 (message, startIndex + 24); + NtChallengeResponse = new byte[ntLength]; + Buffer.BlockCopy (message, startIndex + ntOffset, NtChallengeResponse, 0, ntLength); + payloadOffset = Math.Min (payloadOffset, ntOffset); + + int domainLength = BitConverterLE.ToUInt16 (message, startIndex + 28); + int domainOffset = BitConverterLE.ToUInt16 (message, startIndex + 32); + Domain = DecodeString (message, startIndex + domainOffset, domainLength); + payloadOffset = Math.Min (payloadOffset, domainOffset); + + int userLength = BitConverterLE.ToUInt16 (message, startIndex + 36); + int userOffset = BitConverterLE.ToUInt16 (message, startIndex + 40); + UserName = DecodeString (message, startIndex + userOffset, userLength); + payloadOffset = Math.Min (payloadOffset, userOffset); + + int workstationLength = BitConverterLE.ToUInt16 (message, startIndex + 44); + int workstationOffset = BitConverterLE.ToUInt16 (message, startIndex + 48); + Workstation = DecodeString (message, startIndex + workstationOffset, workstationLength); + payloadOffset = Math.Min (payloadOffset, workstationOffset); + + int skeyLength = BitConverterLE.ToUInt16 (message, startIndex + 52); + int skeyOffset = BitConverterLE.ToUInt16 (message, startIndex + 56); + EncryptedRandomSessionKey = new byte[skeyLength]; + Buffer.BlockCopy (message, startIndex + skeyOffset, EncryptedRandomSessionKey, 0, skeyLength); + payloadOffset = Math.Min (payloadOffset, skeyOffset); + + // OSVersion + if ((Flags & NtlmFlags.NegotiateVersion) != 0 && length >= 72) { + // decode the OS Version + int major = message[startIndex + 64]; + int minor = message[startIndex + 65]; + int build = BitConverterLE.ToUInt16 (message, startIndex + 66); + + OSVersion = new Version (major, minor, build); + micOffset += 8; + } + + // MIC + if (micOffset + 16 <= payloadOffset) { + Mic = new byte[16]; + Buffer.BlockCopy (message, startIndex + micOffset, Mic, 0, Mic.Length); + } + } + + string DecodeString (byte[] buffer, int offset, int len) + { + var encoding = (Flags & NtlmFlags.NegotiateUnicode) != 0 ? Encoding.Unicode : Encoding.UTF8; + + return encoding.GetString (buffer, offset, len); + } + + byte[] EncodeString (string text) + { + if (text == null) + return Array.Empty (); + + var encoding = (Flags & NtlmFlags.NegotiateUnicode) != 0 ? Encoding.Unicode : Encoding.UTF8; + + return encoding.GetBytes (text); + } + + public void ComputeNtlmV2 (string? targetName, bool unverifiedTargetName, byte[]? channelBinding) + { + if (challenge == null || negotiate == null) + return; + + var targetInfo = new NtlmTargetInfo (); + int avFlags = 0; + + // If the CHALLENGE_MESSAGE contains a TargetInfo field + if (challenge.TargetInfo != null) { + challenge.TargetInfo.CopyTo (targetInfo); + + if (targetInfo.Flags.HasValue) + avFlags = targetInfo.Flags.Value; + + // If the CHALLENGE_MESSAGE TargetInfo field (section 2.2.1.2) has an MsvAvTimestamp present, the client SHOULD provide a MIC. + if (challenge.TargetInfo.Timestamp != null) { + // If there is an AV_PAIR structure (section 2.2.2.1) with the AvId field set to MsvAvFlags, then in the Value field, set bit 0x2 to 1. + // Else add an AV_PAIR structure and set the AvId field to MsvAvFlags and the Value field bit 0x2 to 1. + targetInfo.Flags = avFlags |= 0x2; + + // Temporarily set the MIC to Z16. + Mic = Z16; + } + + // If ClientSuppliedTargetName (section 3.1.1.2) is not NULL + if (targetName != null) { + // If UnverifiedTargetName (section 3.1.1.2) is TRUE, then in AvId field = MsvAvFlags set 0x00000004 bit. + if (unverifiedTargetName) + targetInfo.Flags = avFlags |= 0x4; + + // Add an AV_PAIR structure and set the AvId field to MsvAvTargetName and the Value field to ClientSuppliedTargetName without + // terminating NULL. + targetInfo.TargetName = targetName; + } else { + // Else add an AV_PAIR structure and set the AvId field to MsvAvTargetName and the Value field to an empty string without terminating NULL. + targetInfo.TargetName = string.Empty; + } + + // The client SHOULD send the channel binding AV_PAIR: + // If the ClientChannelBindingsUnhashed (section 3.1.1.2) is not NULL + if (channelBinding != null) { + // Add an AV_PAIR structure and set the AvId field to MsvAvChannelBindings and the Value field to MD5_HASH(ClientChannelBindingsUnhashed). + targetInfo.ChannelBinding = NtlmUtils.MD5 (channelBinding); + } else { + // Else add an AV_PAIR structure and set the AvId field to MsvAvChannelBindings and the Value field to Z(16). + targetInfo.ChannelBinding = Z16; + } + } + + var encodedTargetInfo = targetInfo.Encode ((Flags & NtlmFlags.NegotiateUnicode) != 0); + + // Note: For NTLMv2, the sessionBaseKey is the same as the keyExchangeKey. + NtlmUtils.ComputeNtlmV2 (challenge, Domain, UserName, Password, encodedTargetInfo, clientChallenge, Timestamp, out var ntChallengeResponse, out var lmChallengeResponse, out var keyExchangeKey); + + NtChallengeResponse = ntChallengeResponse; + LmChallengeResponse = lmChallengeResponse; + + if ((Flags & NtlmFlags.NegotiateKeyExchange) != 0 && (Flags & (NtlmFlags.NegotiateSign | NtlmFlags.NegotiateSeal)) != 0) { + ExportedSessionKey = NtlmUtils.NONCE (16); + EncryptedRandomSessionKey = NtlmUtils.RC4K (keyExchangeKey, ExportedSessionKey); + } else { + ExportedSessionKey = keyExchangeKey; + EncryptedRandomSessionKey = null; + } + + // If the CHALLENGE_MESSAGE TargetInfo field (section 2.2.1.2) has an MsvAvTimestamp present, the client SHOULD provide a MIC. + if ((avFlags & 0x2) != 0) + Mic = NtlmUtils.HMACMD5 (ExportedSessionKey, NtlmUtils.ConcatenationOf (negotiate.Encode (), challenge.Encode (), Encode ())); + } + + public override byte[] Encode () + { + var target = EncodeString (Domain); + var user = EncodeString (UserName); + var workstation = EncodeString (Workstation); + int payloadOffset = 72, micOffset = -1; + + if (Mic != null) { + micOffset = payloadOffset; + payloadOffset += Mic.Length; + } + + var lmResponseLength = LmChallengeResponse != null ? LmChallengeResponse.Length : 0; + var ntResponseLength = NtChallengeResponse != null ? NtChallengeResponse.Length : 0; + int skeyLength = EncryptedRandomSessionKey != null ? EncryptedRandomSessionKey.Length : 0; + + var message = PrepareMessage (payloadOffset + target.Length + user.Length + workstation.Length + lmResponseLength + ntResponseLength + skeyLength); + + // LmChallengeResponse + short lmResponseOffset = (short) (payloadOffset + target.Length + user.Length + workstation.Length); + message[12] = (byte) lmResponseLength; + message[13] = (byte) 0x00; + message[14] = message[12]; + message[15] = message[13]; + message[16] = (byte) lmResponseOffset; + message[17] = (byte) (lmResponseOffset >> 8); + //message[18] = (byte) (lmResponseOffset >> 16); + //message[19] = (byte) (lmResponseOffset >> 24); + + // NtChallengeResponse + short ntResponseOffset = (short) (lmResponseOffset + lmResponseLength); + message[20] = (byte) ntResponseLength; + message[21] = (byte) (ntResponseLength >> 8); + message[22] = message[20]; + message[23] = message[21]; + message[24] = (byte) ntResponseOffset; + message[25] = (byte) (ntResponseOffset >> 8); + //message[26] = (byte) (ntResponseOffset >> 16); + //message[27] = (byte) (ntResponseOffset >> 24); + + // Target + short domainLength = (short) target.Length; + short domainOffset = (short) payloadOffset; + message[28] = (byte) domainLength; + message[29] = (byte) (domainLength >> 8); + message[30] = message[28]; + message[31] = message[29]; + message[32] = (byte) domainOffset; + message[33] = (byte) (domainOffset >> 8); + //message[34] = (byte) (domainOffset >> 16); + //message[35] = (byte) (domainOffset >> 24); + + // UserName + short userLength = (short) user.Length; + short userOffset = (short) (domainOffset + domainLength); + message[36] = (byte) userLength; + message[37] = (byte) (userLength >> 8); + message[38] = message[36]; + message[39] = message[37]; + message[40] = (byte) userOffset; + message[41] = (byte) (userOffset >> 8); + //message[42] = (byte) (userOffset >> 16); + //message[43] = (byte) (userOffset >> 24); + + // Workstation + short workstationLength = (short) workstation.Length; + short workstationOffset = (short) (userOffset + userLength); + message[44] = (byte) workstationLength; + message[45] = (byte) (workstationLength >> 8); + message[46] = message[44]; + message[47] = message[45]; + message[48] = (byte) workstationOffset; + message[49] = (byte) (workstationOffset >> 8); + //message[50] = (byte) (workstationOffset >> 16); + //message[51] = (byte) (workstationOffset >> 24); + + // EncryptedRandomSessionKey + short skeyOffset = (short) (ntResponseOffset + ntResponseLength); + message[52] = (byte) skeyLength; + message[53] = (byte) (skeyLength >> 8); + message[54] = message[52]; + message[55] = message[53]; + message[56] = (byte) skeyOffset; + message[57] = (byte) (skeyOffset >> 8); + //message[58] = (byte) (skeyOffset >> 16); + //message[59] = (byte) (skeyOffset >> 24); + + // options flags + message[60] = (byte) Flags; + message[61] = (byte)((uint) Flags >> 8); + message[62] = (byte)((uint) Flags >> 16); + message[63] = (byte)((uint) Flags >> 24); + + if (challenge != null && (challenge.Flags & NtlmFlags.NegotiateVersion) != 0) { + if (OSVersion != null) { + message[64] = (byte) OSVersion.Major; + message[65] = (byte) OSVersion.Minor; + message[66] = (byte) OSVersion.Build; + message[67] = (byte)(OSVersion.Build >> 8); + } + message[68] = 0x00; + message[69] = 0x00; + message[70] = 0x00; + message[71] = 0x0f; + } + + if (Mic != null) + Buffer.BlockCopy (Mic, 0, message, micOffset, Mic.Length); + + Buffer.BlockCopy (target, 0, message, domainOffset, target.Length); + Buffer.BlockCopy (user, 0, message, userOffset, user.Length); + Buffer.BlockCopy (workstation, 0, message, workstationOffset, workstation.Length); + + if (LmChallengeResponse != null) + Buffer.BlockCopy (LmChallengeResponse, 0, message, lmResponseOffset, LmChallengeResponse.Length); + + if (NtChallengeResponse != null) + Buffer.BlockCopy (NtChallengeResponse, 0, message, ntResponseOffset, NtChallengeResponse.Length); + + if ((Flags & NtlmFlags.NegotiateKeyExchange) != 0 && EncryptedRandomSessionKey != null) + Buffer.BlockCopy (EncryptedRandomSessionKey, 0, message, skeyOffset, EncryptedRandomSessionKey.Length); + + return message; + } + } +} diff --git a/MailKit/Security/Ntlm/NtlmChallengeMessage.cs b/MailKit/Security/Ntlm/NtlmChallengeMessage.cs new file mode 100644 index 0000000000..f281502485 --- /dev/null +++ b/MailKit/Security/Ntlm/NtlmChallengeMessage.cs @@ -0,0 +1,213 @@ +// +// NtlmChallengeMessage.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/b38c36ed-2804-4868-a9ff-8dd3182128e4 + +using System; +using System.Text; + +namespace MailKit.Security.Ntlm { + class NtlmChallengeMessage : NtlmMessageBase + { + const NtlmFlags DefaultFlags = NtlmFlags.NegotiateNtlm | NtlmFlags.NegotiateUnicode /*| NtlmFlags.NegotiateAlwaysSign*/; + byte[] serverChallenge; + byte[]? cached; + + public NtlmChallengeMessage (NtlmFlags flags, Version? osVersion = null) : base (2) + { + serverChallenge = NtlmUtils.NONCE (8); + OSVersion = osVersion; + Flags = flags; + } + + public NtlmChallengeMessage (Version? osVersion = null) : this (DefaultFlags, osVersion) + { + } + + public NtlmChallengeMessage (byte[] message, int startIndex, int length) : base (2) + { + serverChallenge = new byte[8]; + Decode (message, startIndex, length); + + cached = new byte[length]; + Buffer.BlockCopy (message, startIndex, cached, 0, length); + } + + ~NtlmChallengeMessage () + { + if (serverChallenge != null) + Array.Clear (serverChallenge, 0, serverChallenge.Length); + } + + public byte[] ServerChallenge { + get { return serverChallenge; } + set { + if (value == null) + throw new ArgumentNullException (nameof (value)); + + if (value.Length != 8) + throw new ArgumentException ("Invalid nonce length (should be 8 bytes).", nameof (value)); + + Array.Clear (serverChallenge, 0, serverChallenge.Length); + serverChallenge = value; + } + } + + public string? TargetName { + get; set; + } + + public NtlmTargetInfo? TargetInfo { + get; set; + } + + public byte[]? GetEncodedTargetInfo () + { + return TargetInfo?.Encode ((Flags & NtlmFlags.NegotiateUnicode) != 0); + } + + void Decode (byte[] message, int startIndex, int length) + { + ValidateArguments (message, startIndex, length); + + Flags = (NtlmFlags) BitConverterLE.ToUInt32 (message, startIndex + 20); + + Buffer.BlockCopy (message, startIndex + 24, serverChallenge, 0, 8); + + var targetNameLength = BitConverterLE.ToUInt16 (message, startIndex + 12); + //var targetNameMaxLength = BitConverterLE.ToUInt16 (message, startIndex + 14); + var targetNameOffset = BitConverterLE.ToInt32 (message, startIndex + 16); + + if (targetNameLength > 0) { + var encoding = (Flags & NtlmFlags.NegotiateUnicode) != 0 ? Encoding.Unicode : Encoding.UTF8; + + TargetName = encoding.GetString (message, startIndex + targetNameOffset, targetNameLength); + } + + if ((Flags & NtlmFlags.NegotiateVersion) != 0 && length >= 56) { + // decode the OS Version + int major = message[startIndex + 48]; + int minor = message[startIndex + 49]; + int build = BitConverterLE.ToUInt16 (message, startIndex + 50); + + OSVersion = new Version (major, minor, build); + } + + // The Target Info block is optional. + if (length >= 48 && targetNameOffset >= 48) { + var targetInfoLength = BitConverterLE.ToUInt16 (message, startIndex + 40); + var targetInfoOffset = BitConverterLE.ToUInt16 (message, startIndex + 44); + + if (targetInfoLength > 0 && targetInfoOffset < length && targetInfoLength <= (length - targetInfoOffset)) + TargetInfo = new NtlmTargetInfo (message, startIndex + targetInfoOffset, targetInfoLength, (Flags & NtlmFlags.NegotiateUnicode) != 0); + } + } + + public override byte[] Encode () + { + if (cached != null) + return cached; + + var targetInfo = GetEncodedTargetInfo (); + int targetNameOffset = 48; + int targetInfoOffset = 56; + byte[]? targetName = null; + int size = 48; + + if (TargetName != null) { + var encoding = (Flags & NtlmFlags.NegotiateUnicode) != 0 ? Encoding.Unicode : Encoding.UTF8; + + targetName = encoding.GetBytes (TargetName); + targetInfoOffset += targetName.Length; + size += targetName.Length; + } + + if (targetInfo != null) { + size += 8 + targetInfo.Length; + targetNameOffset += 8; + } + + // 12 bytes + var message = PrepareMessage (size); + + // TargetName (8 bytes) + if (targetName != null) { + message[12] = (byte) targetName.Length; + message[13] = (byte)(targetName.Length >> 8); + message[14] = (byte)targetName.Length; + message[15] = (byte)(targetName.Length >> 8); + message[16] = (byte) targetNameOffset; + message[17] = (byte)(targetNameOffset >> 8); + //message[18] = (byte) (targetNameOffset >> 16); + //message[19] = (byte) (targetNameOffset >> 24); + + // TargetName Payload + Buffer.BlockCopy (targetName, 0, message, targetNameOffset, targetName.Length); + } + + // NegotiateFlags (4 bytes) + message[20] = (byte) Flags; + message[21] = (byte) ((uint) Flags >> 8); + message[22] = (byte) ((uint) Flags >> 16); + message[23] = (byte) ((uint) Flags >> 24); + + // ServerChallenge (8 bytes) + Buffer.BlockCopy (serverChallenge, 0, message, 24, serverChallenge.Length); + + // Reserved (8 bytes) + + // TargetInfo (8 bytes) + if (targetInfo != null) { + message[40] = (byte) targetInfo.Length; + message[41] = (byte)(targetInfo.Length >> 8); + message[42] = (byte) targetInfo.Length; + message[43] = (byte)(targetInfo.Length >> 8); + message[44] = (byte) targetInfoOffset; + message[45] = (byte)(targetInfoOffset >> 8); + + // TargetInfo Payload + Buffer.BlockCopy (targetInfo, 0, message, targetInfoOffset, targetInfo.Length); + } + + if ((Flags & NtlmFlags.NegotiateVersion) != 0) { + if (OSVersion != null) { + message[48] = (byte) OSVersion.Major; + message[49] = (byte) OSVersion.Minor; + message[50] = (byte) OSVersion.Build; + message[51] = (byte)(OSVersion.Build >> 8); + } + message[52] = 0x00; + message[53] = 0x00; + message[54] = 0x00; + message[55] = 0x0f; + } + + cached = message; + + return message; + } + } +} diff --git a/MailKit/Security/Ntlm/NtlmFlags.cs b/MailKit/Security/Ntlm/NtlmFlags.cs index d1ef232d09..2a45ec4fa0 100644 --- a/MailKit/Security/Ntlm/NtlmFlags.cs +++ b/MailKit/Security/Ntlm/NtlmFlags.cs @@ -1,9 +1,9 @@ -// +// // NtlmFlags.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -24,6 +24,8 @@ // THE SOFTWARE. // +// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/b38c36ed-2804-4868-a9ff-8dd3182128e4 + using System; namespace MailKit.Security.Ntlm { @@ -148,13 +150,18 @@ enum NtlmFlags { R6 = TargetTypeShare, /// - /// Indicates that the NTLM2 signing and sealing scheme should be used for - /// protecting authenticated communications. Note that this refers to a - /// particular session security scheme, and is not related to the use of - /// NTLMv2 authentication. This flag can, however, have an effect on the - /// response calculations. + /// If set, requests usage of the NTLM v2 session security. NTLM v2 session + /// security is a misnomer because it is not NTLM v2. It is NTLM v1 using the + /// extended session security that is also in NTLM v2. NTLMSSP_NEGOTIATE_LM_KEY + /// and NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY are mutually exclusive. If + /// both NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY and NTLMSSP_NEGOTIATE_LM_KEY + /// are requested, NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY alone MUST be + /// returned to the client. NTLM v2 authentication session key generation MUST + /// be supported by both the client and the DC in order to be used, and extended + /// session security signing and sealing requires support from the client and + /// the server in order to be used. /// - NegotiateNtlm2Key = 0x00080000, + NegotiateExtendedSessionSecurity = 0x00080000, /// /// This flag's usage has not been identified. diff --git a/MailKit/Security/Ntlm/NtlmMessageBase.cs b/MailKit/Security/Ntlm/NtlmMessageBase.cs new file mode 100644 index 0000000000..e9dd66bcaa --- /dev/null +++ b/MailKit/Security/Ntlm/NtlmMessageBase.cs @@ -0,0 +1,99 @@ +// +// NtlmMessageBase.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/b38c36ed-2804-4868-a9ff-8dd3182128e4 + +using System; +using System.Globalization; + +namespace MailKit.Security.Ntlm { + abstract class NtlmMessageBase + { + static readonly byte[] Signature = { (byte) 'N', (byte) 'T', (byte) 'L', (byte) 'M', (byte) 'S', (byte) 'S', (byte) 'P', 0x00 }; + + protected NtlmMessageBase (int type) + { + Type = type; + } + + public NtlmFlags Flags { + get; protected set; + } + + public Version? OSVersion { + get; protected set; + } + + public int Type { + get; private set; + } + + protected byte[] PrepareMessage (int size) + { + var message = new byte[size]; + + Buffer.BlockCopy (Signature, 0, message, 0, 8); + + message[ 8] = (byte) Type; + message[ 9] = (byte)(Type >> 8); + message[10] = (byte)(Type >> 16); + message[11] = (byte)(Type >> 24); + + return message; + } + + bool CheckSignature (byte[] message, int startIndex) + { + for (int i = 0; i < Signature.Length; i++) { + if (message[startIndex + i] != Signature[i]) + return false; + } + + return BitConverterLE.ToUInt32 (message, startIndex + 8) == Type; + } + + protected void ValidateArguments (byte[] message, int startIndex, int length) + { + if (message == null) + throw new ArgumentNullException (nameof (message)); + + if (startIndex < 0 || startIndex > message.Length) + throw new ArgumentOutOfRangeException (nameof (startIndex)); + + if (length < 12 || length > (message.Length - startIndex)) + throw new ArgumentOutOfRangeException (nameof (length)); + + if (!CheckSignature (message, startIndex)) + throw new ArgumentException (string.Format (CultureInfo.InvariantCulture, "Invalid Type{0} message.", Type), nameof (message)); + + var messageType = BitConverterLE.ToUInt32 (message, 8); + if (messageType != Type) + throw new ArgumentException (string.Format (CultureInfo.InvariantCulture, "Invalid Type{0} message.", Type), nameof (message)); + } + + public abstract byte[] Encode (); + } +} diff --git a/MailKit/Security/Ntlm/NtlmNegotiateMessage.cs b/MailKit/Security/Ntlm/NtlmNegotiateMessage.cs new file mode 100644 index 0000000000..6ebb79abc4 --- /dev/null +++ b/MailKit/Security/Ntlm/NtlmNegotiateMessage.cs @@ -0,0 +1,173 @@ +// +// NtlmNegotiateMessage.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +// NTLM specification: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/b38c36ed-2804-4868-a9ff-8dd3182128e4 +// +// NTLM registry key documentation: https://learn.microsoft.com/en-us/troubleshoot/windows-client/windows-security/enable-ntlm-2-authentication +// Note: Ideally, we'd default our flags based on the registry settings of the OS. + +using System; +using System.Text; +using System.Diagnostics.CodeAnalysis; + +namespace MailKit.Security.Ntlm { + class NtlmNegotiateMessage : NtlmMessageBase + { + // System.Net.Mail seems to default to: NtlmFlags.Negotiate56 | NtlmFlags.NegotiateUnicode | NtlmFlags.NegotiateOem | NtlmFlags.RequestTarget | NtlmFlags.NegotiateNtlm | NtlmFlags.NegotiateAlwaysSign | NtlmFlags.NegotiateExtendedSessionSecurity | NtlmFlags.NegotiateVersion | NtlmFlags.Negotiate128 + internal const NtlmFlags DefaultFlags = NtlmFlags.Negotiate56 | NtlmFlags.NegotiateUnicode | NtlmFlags.NegotiateOem | NtlmFlags.RequestTarget | NtlmFlags.NegotiateNtlm | NtlmFlags.NegotiateAlwaysSign | NtlmFlags.NegotiateExtendedSessionSecurity | NtlmFlags.Negotiate128; + + byte[]? cached; + + public NtlmNegotiateMessage (NtlmFlags flags, string? domain, string? workstation, Version? osVersion = null) : base (1) + { + Flags = flags & ~(NtlmFlags.NegotiateDomainSupplied | NtlmFlags.NegotiateWorkstationSupplied | NtlmFlags.NegotiateVersion); + + // Note: If the NTLMSSP_NEGOTIATE_VERSION flag is set by the client application, the Version field + // MUST be set to the current version (section 2.2.2.10), the DomainName field MUST be set to + // a zero-length string, and the Workstation field MUST be set to a zero-length string. + if (osVersion != null) { + Flags |= NtlmFlags.NegotiateVersion; + Workstation = string.Empty; + Domain = string.Empty; + OSVersion = osVersion; + } else { + if (!string.IsNullOrEmpty (workstation)) { + Flags |= NtlmFlags.NegotiateWorkstationSupplied; + Workstation = workstation!.ToUpperInvariant (); + } else { + Workstation = string.Empty; + } + + if (!string.IsNullOrEmpty (domain)) { + Flags |= NtlmFlags.NegotiateDomainSupplied; + Domain = domain!.ToUpperInvariant (); + } else { + Domain = string.Empty; + } + } + } + + public NtlmNegotiateMessage (string? domain = null, string? workstation = null, Version? osVersion = null) : this (DefaultFlags, domain, workstation, osVersion) + { + } + + public NtlmNegotiateMessage (byte[] message, int startIndex, int length) : base (1) + { + Decode (message, startIndex, length); + + cached = new byte[length]; + Buffer.BlockCopy (message, startIndex, cached, 0, length); + } + + public string Domain { + get; private set; + } + + public string Workstation { + get; private set; + } + + [MemberNotNull (nameof (Domain), nameof (Workstation))] + void Decode (byte[] message, int startIndex, int length) + { + ValidateArguments (message, startIndex, length); + + Flags = (NtlmFlags) BitConverterLE.ToUInt32 (message, startIndex + 12); + + // decode the domain + var domainLength = BitConverterLE.ToUInt16 (message, startIndex + 16); + var domainOffset = BitConverterLE.ToUInt16 (message, startIndex + 20); + Domain = Encoding.UTF8.GetString (message, startIndex + domainOffset, domainLength); + + // decode the workstation/host + var workstationLength = BitConverterLE.ToUInt16 (message, startIndex + 24); + var workstationOffset = BitConverterLE.ToUInt16 (message, startIndex + 28); + Workstation = Encoding.UTF8.GetString (message, startIndex + workstationOffset, workstationLength); + + if ((Flags & NtlmFlags.NegotiateVersion) != 0 && length >= 40) { + // decode the OS Version + int major = message[startIndex + 32]; + int minor = message[startIndex + 33]; + int build = BitConverterLE.ToUInt16 (message, startIndex + 34); + + OSVersion = new Version (major, minor, build); + } + } + + public override byte[] Encode () + { + if (cached != null) + return cached; + + var workstation = Encoding.UTF8.GetBytes (Workstation); + var domain = Encoding.UTF8.GetBytes (Domain); + const int versionLength = 8; + int workstationOffset = 32 + versionLength; + int domainOffset = workstationOffset + workstation.Length; + + var message = PrepareMessage (32 + domain.Length + workstation.Length + versionLength); + + message[12] = (byte) Flags; + message[13] = (byte)((uint) Flags >> 8); + message[14] = (byte)((uint) Flags >> 16); + message[15] = (byte)((uint) Flags >> 24); + + message[16] = (byte) domain.Length; + message[17] = (byte)(domain.Length >> 8); + message[18] = message[16]; + message[19] = message[17]; + message[20] = (byte) domainOffset; + message[21] = (byte)(domainOffset >> 8); + + message[24] = (byte) workstation.Length; + message[25] = (byte)(workstation.Length >> 8); + message[26] = message[24]; + message[27] = message[25]; + message[28] = (byte) workstationOffset; + message[29] = (byte)(workstationOffset >> 8); + + if ((Flags & NtlmFlags.NegotiateVersion) != 0) { + if (OSVersion != null) { + message[32] = (byte) OSVersion.Major; + message[33] = (byte) OSVersion.Minor; + message[34] = (byte) OSVersion.Build; + message[35] = (byte)(OSVersion.Build >> 8); + } + message[36] = 0x00; + message[37] = 0x00; + message[38] = 0x00; + message[39] = 0x0f; + } + + Buffer.BlockCopy (workstation, 0, message, workstationOffset, workstation.Length); + Buffer.BlockCopy (domain, 0, message, domainOffset, domain.Length); + + cached = message; + + return message; + } + } +} diff --git a/MailKit/Security/Ntlm/NtlmSingleHostData.cs b/MailKit/Security/Ntlm/NtlmSingleHostData.cs new file mode 100644 index 0000000000..325cbefc68 --- /dev/null +++ b/MailKit/Security/Ntlm/NtlmSingleHostData.cs @@ -0,0 +1,186 @@ +// +// NtlmSingleHostData.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/b38c36ed-2804-4868-a9ff-8dd3182128e4 + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace MailKit.Security.Ntlm { + /// + /// An NTLM SingleHostData structure. + /// + /// + /// An NTLM SingleHostData structure. + /// + class NtlmSingleHostData + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The raw target info buffer to decode. + /// The starting index of the single host data structure. + /// The length of the single host data structure. + public NtlmSingleHostData (byte[] buffer, int startIndex, int length) + { + Decode (buffer, startIndex, length); + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The 8-byte platform-specific blob. + /// The 256-bit randomly generated machine id. + /// + /// is . + /// -or- + /// is . + /// + /// + /// is not 8 bytes. + /// -or- + /// is not 32 bytes. + /// + public NtlmSingleHostData (byte[] customData, byte[] machineId) + { + if (customData == null) + throw new ArgumentNullException (nameof (customData)); + + if (customData.Length != 8) + throw new ArgumentException ("The custom data must be 8 bytes.", nameof (customData)); + + if (machineId == null) + throw new ArgumentNullException (nameof (machineId)); + + if (machineId.Length != 32) + throw new ArgumentException ("The machine id must be 32 bytes.", nameof (machineId)); + + CustomData = customData; + MachineId = machineId; + Size = 48; + } + + /// + /// Get or set an 8-byte platform-specific blob. + /// + /// + /// Gets or sets an 8-byte platform-specific blob. + /// + public byte[] CustomData { + get; private set; + } + + /// + /// Get the 256-bit randomly generated machine ID. + /// + /// + /// Gets the 256-bit randomly generated machine ID. + /// + /// The 256-bit randomly generated machine ID. + public byte[] MachineId { + get; private set; + } + + /// + /// Get the size of the SingleHostData structure. + /// + /// + /// Gets the size of the SingleHostData structure. + /// + /// The size of the SingleHostData structure. + public int Size { + get; private set; + } + + [MemberNotNull (nameof (CustomData), nameof (MachineId))] + void Decode (byte[] buffer, int startIndex, int length) + { + if (buffer == null) + throw new ArgumentNullException (nameof (buffer)); + + if (startIndex < 0 || startIndex > buffer.Length) + throw new ArgumentOutOfRangeException (nameof (startIndex)); + + if (length < 48 || length > (buffer.Length - startIndex)) + throw new ArgumentOutOfRangeException (nameof (length)); + + int index = startIndex; + + // Size (4 bytes): A 32-bit unsigned integer that defines the length, in bytes, of the Value field in the AV_PAIR (section 2.2.2.1) structure. + Size = BitConverterLE.ToInt32 (buffer, index); + index += 4; + + // Z4 (4 bytes): A 32-bit integer value containing 0x00000000. + index += 4; + + // CustomData (8 bytes): An 8-byte platform-specific blob containing info only relevant when the client and the server are on the same host. + CustomData = new byte[8]; + Buffer.BlockCopy (buffer, index, CustomData, 0, 8); + index += 8; + + // MachineID (32 bytes): A 256-bit random number created at computer startup to identify the calling machine. + MachineId = new byte[32]; + Buffer.BlockCopy (buffer, index, MachineId, 0, 32); + } + + /// + /// Encode the SingleHostData structure. + /// + /// + /// Encodes the SingleHostData structure. + /// + /// The encoded SingleHostData structure. + public byte[] Encode () + { + var buffer = new byte[Size]; + int index = 0; + + // Size (4 bytes): A 32-bit unsigned integer that defines the length, in bytes, of the Value field in the AV_PAIR (section 2.2.2.1) structure. + buffer[index++] = (byte) (Size); + buffer[index++] = (byte) (Size >> 8); + buffer[index++] = (byte) (Size >> 16); + buffer[index++] = (byte) (Size >> 24); + + // Z4 (4 bytes): A 32-bit integer value containing 0x00000000. + index += 4; + + // CustomData (8 bytes): An 8-byte platform-specific blob containing info only relevant when the client and the server are on the same host. + Buffer.BlockCopy (CustomData, 0, buffer, index, 8); + index += 8; + + // MachineID (32 bytes): A 256-bit random number created at computer startup to identify the calling machine. + Buffer.BlockCopy (MachineId, 0, buffer, index, 32); + + return buffer; + } + } +} diff --git a/MailKit/Security/Ntlm/NtlmTargetInfo.cs b/MailKit/Security/Ntlm/NtlmTargetInfo.cs new file mode 100644 index 0000000000..bdc266d355 --- /dev/null +++ b/MailKit/Security/Ntlm/NtlmTargetInfo.cs @@ -0,0 +1,427 @@ +// +// NtlmTargetInfo.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/b38c36ed-2804-4868-a9ff-8dd3182128e4 + +using System; +using System.Text; +using System.Collections.Generic; + +namespace MailKit.Security.Ntlm { + /// + /// An NTLM TargetInfo structure. + /// + /// + /// An NTLM TargetInfo structure. + /// + class NtlmTargetInfo + { + readonly List attributes = new List (); + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The raw target info buffer to decode. + /// The starting index of the target info structure. + /// The length of the target info structure. + /// if the target info strings are unicode; otherwise, . + public NtlmTargetInfo (byte[] buffer, int startIndex, int length, bool unicode) + { + Decode (buffer, startIndex, length, unicode); + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + public NtlmTargetInfo () + { + } + + /// + /// Copy the attribute value pairs to another TargetInfo. + /// + /// + /// Copies the attribute value pairs to another TargetInfo. + /// + public void CopyTo (NtlmTargetInfo targetInfo) + { + targetInfo.attributes.Clear (); + + foreach (var attribute in attributes) { + if (attribute is NtlmAttributeTimestampValuePair timestamp) + targetInfo.attributes.Add (new NtlmAttributeTimestampValuePair (timestamp.Attribute, timestamp.Value, timestamp.Size)); + else if (attribute is NtlmAttributeFlagsValuePair flags) + targetInfo.attributes.Add (new NtlmAttributeFlagsValuePair (flags.Attribute, flags.Value, flags.Size)); + else if (attribute is NtlmAttributeByteArrayValuePair array) + targetInfo.attributes.Add (new NtlmAttributeByteArrayValuePair (array.Attribute, array.Value)); + else if (attribute is NtlmAttributeStringValuePair str) + targetInfo.attributes.Add (new NtlmAttributeStringValuePair (str.Attribute, str.Value)); + } + } + + internal NtlmAttributeValuePair? GetAvPair (NtlmAttribute attr) + { + for (int i = 0; i < attributes.Count; i++) { + if (attributes[i].Attribute == attr) + return attributes[i]; + } + + return null; + } + + string? GetAvPairString (NtlmAttribute attr) + { + return ((NtlmAttributeStringValuePair?) GetAvPair (attr))?.Value; + } + + void SetAvPairString (NtlmAttribute attr, string? value) + { + var pair = (NtlmAttributeStringValuePair?) GetAvPair (attr); + + if (pair == null) { + if (value != null) + attributes.Add (new NtlmAttributeStringValuePair (attr, value)); + } else if (value != null) { + pair.Value = value; + } else { + attributes.Remove (pair); + } + } + + byte[]? GetAvPairByteArray (NtlmAttribute attr) + { + return ((NtlmAttributeByteArrayValuePair?) GetAvPair (attr))?.Value; + } + + void SetAvPairByteArray (NtlmAttribute attr, byte[]? value) + { + var pair = (NtlmAttributeByteArrayValuePair?) GetAvPair (attr); + + if (pair == null) { + if (value != null) + attributes.Add (new NtlmAttributeByteArrayValuePair (attr, value)); + } else if (value != null) { + pair.Value = value; + } else { + attributes.Remove (pair); + } + } + + /// + /// Get or set the server's NetBIOS computer name. + /// + /// + /// Gets or sets the server's NetBIOS computer name. + /// + /// The server's NetBIOS computer name if available; otherwise, . + public string? ServerName { + get { return GetAvPairString (NtlmAttribute.ServerName); } + set { SetAvPairString (NtlmAttribute.ServerName, value); } + } + + /// + /// Get or set the server's NetBIOS domain name. + /// + /// + /// Gets or sets the server's NetBIOS domain name. + /// + /// The server's NetBIOS domain name if available; otherwise, . + public string? DomainName { + get { return GetAvPairString (NtlmAttribute.DomainName); } + set { SetAvPairString (NtlmAttribute.DomainName, value); } + } + + /// + /// Get or set the fully qualified domain name (FQDN) of the server. + /// + /// + /// Gets or sets the fully qualified domain name (FQDN) of the server. + /// + /// The fully qualified domain name (FQDN) of the server if available; otherwise, . + public string? DnsServerName { + get { return GetAvPairString (NtlmAttribute.DnsServerName); } + set { SetAvPairString (NtlmAttribute.DnsServerName, value); } + } + + /// + /// Get or set the fully qualified domain name (FQDN) of the domain. + /// + /// + /// Gets or sets the fully qualified domain name (FQDN) of the domain. + /// + /// The fully qualified domain name (FQDN) of the domain if available; otherwise, . + public string? DnsDomainName { + get { return GetAvPairString (NtlmAttribute.DnsDomainName); } + set { SetAvPairString (NtlmAttribute.DnsDomainName, value); } + } + + /// + /// Get or set the fully qualified domain name (FQDN) of the forest. + /// + /// + /// Gets or sets the fully qualified domain name (FQDN) of the forest. + /// + /// The fully qualified domain name (FQDN) of the forest if available; otherwise, . + public string? DnsTreeName { + get { return GetAvPairString (NtlmAttribute.DnsTreeName); } + set { SetAvPairString (NtlmAttribute.DnsTreeName, value); } + } + + /// + /// Get or set a 32-bit value indicating server or client configuration. + /// + /// + /// Gets or sets a 32-bit value indicating server or client configuration. + /// 0x00000001: Indicates to the client that the account authentication is constrained. + /// 0x00000002: Indicates that the client is providing message integrity in the MIC field (section 2.2.1.3) in the AUTHENTICATE_MESSAGE. + /// 0x00000004: Indicates that the client is providing a target SPN generated from an untrusted source. + /// + /// The 32-bit flags value if available; otherwise, . + public int? Flags { + get { return ((NtlmAttributeFlagsValuePair?) GetAvPair (NtlmAttribute.Flags))?.Value; } + set { + var pair = (NtlmAttributeFlagsValuePair?) GetAvPair (NtlmAttribute.Flags); + + if (pair == null) { + if (value != null) + attributes.Add (new NtlmAttributeFlagsValuePair (NtlmAttribute.Flags, value.Value)); + } else if (value != null) { + pair.Size = Math.Max (pair.Size, (short) (value.Value > short.MaxValue ? 4 : 2)); + pair.Value = value.Value; + } else { + attributes.Remove (pair); + } + } + } + + /// + /// Get or set a timestamp that contains the server local time. + /// + /// + /// Gets or sets a timestamp that contains the server local time. + /// A FILETIME structure ([MS-DTYP] section 2.3.3) in little-endian byte order that contains + /// the server local time. This structure is always sent in the CHALLENGE_MESSAGE. + /// + /// The local time of the server, if available; otherwise . + public long? Timestamp { + get { return ((NtlmAttributeTimestampValuePair?) GetAvPair (NtlmAttribute.Timestamp))?.Value; } + set { + var pair = (NtlmAttributeTimestampValuePair?) GetAvPair (NtlmAttribute.Timestamp); + + if (pair == null) { + if (value != null) + attributes.Add (new NtlmAttributeTimestampValuePair (NtlmAttribute.Timestamp, value.Value)); + } else if (value != null) { + pair.Size = Math.Max (pair.Size, (short) (value.Value > int.MaxValue ? 8 : 4)); + pair.Value = value.Value; + } else { + attributes.Remove (pair); + } + } + } + + /// + /// Get or set the single host data structure. + /// + /// + /// Gets or sets the single host data structure. + /// The Value field contains a platform-specific blob, as well as a MachineID created at computer startup to identify the calling machine. + /// + /// The single host data structure, if available; otherwise, . + public byte[]? SingleHost { + get { return GetAvPairByteArray (NtlmAttribute.SingleHost); } + set { SetAvPairByteArray (NtlmAttribute.SingleHost, value); } + } + + /// + /// Get or set the Service Principal Name (SPN) of the server. + /// + /// + /// Gets or sets the Service Principal Name (SPN) of the server. + /// + /// The Service Principal Name (SPN) of the server, if available; otherwise, . + public string? TargetName { + get { return GetAvPairString (NtlmAttribute.TargetName); } + set { SetAvPairString (NtlmAttribute.TargetName, value); } + } + + /// + /// Get or set the channel binding hash. + /// + /// + /// Gets or sets the channel binding hash. + /// + /// An MD5 hash of the channel binding data, if available; otherwise . + public byte[]? ChannelBinding { + get { return GetAvPairByteArray (NtlmAttribute.ChannelBinding); } + set { SetAvPairByteArray (NtlmAttribute.ChannelBinding, value); } + } + + static byte[] DecodeByteArray (byte[] buffer, ref int index) + { + var length = BitConverterLE.ToInt16 (buffer, index); + var value = new byte[length]; + + Buffer.BlockCopy (buffer, index + 2, value, 0, length); + + index += 2 + length; + + return value; + } + + static string DecodeString (byte[] buffer, ref int index, bool unicode) + { + var encoding = unicode ? Encoding.Unicode : Encoding.UTF8; + var length = BitConverterLE.ToInt16 (buffer, index); + var value = encoding.GetString (buffer, index + 2, length); + + index += 2 + length; + + return value; + } + + static int DecodeFlags (byte[] buffer, ref int index, out short size) + { + size = BitConverterLE.ToInt16 (buffer, index); + int flags; + + index += 2; + + switch (size) { + case 4: flags = BitConverterLE.ToInt32 (buffer, index); break; + case 2: flags = BitConverterLE.ToInt16 (buffer, index); break; + default: flags = 0; break; + } + + index += size; + + return flags; + } + + static long DecodeTimestamp (byte[] buffer, ref int index, out short size) + { + size = BitConverterLE.ToInt16 (buffer, index); + long value; + + index += 2; + + switch (size) { + case 8: + long lo = BitConverterLE.ToUInt32 (buffer, index); + long hi = BitConverterLE.ToUInt32 (buffer, index + 4); + value = (hi << 32) | lo; + break; + case 4: value = BitConverterLE.ToUInt32 (buffer, index); break; + case 2: value = BitConverterLE.ToUInt16 (buffer, index); break; + default: value = 0; break; + } + + index += size; + + return value; + } + + void Decode (byte[] buffer, int startIndex, int length, bool unicode) + { + if (buffer == null) + throw new ArgumentNullException (nameof (buffer)); + + if (startIndex < 0 || startIndex > buffer.Length) + throw new ArgumentOutOfRangeException (nameof (startIndex)); + + if (length < 12 || length > (buffer.Length - startIndex)) + throw new ArgumentOutOfRangeException (nameof (length)); + + int index = startIndex; + + do { + var attr = (NtlmAttribute) BitConverterLE.ToInt16 (buffer, index); + short size; + + index += 2; + + switch (attr) { + case NtlmAttribute.EOL: + index = startIndex + length; + break; + case NtlmAttribute.ServerName: + case NtlmAttribute.DomainName: + case NtlmAttribute.DnsServerName: + case NtlmAttribute.DnsDomainName: + case NtlmAttribute.DnsTreeName: + case NtlmAttribute.TargetName: + attributes.Add (new NtlmAttributeStringValuePair (attr, DecodeString (buffer, ref index, unicode))); + break; + case NtlmAttribute.Flags: + attributes.Add (new NtlmAttributeFlagsValuePair (attr, DecodeFlags (buffer, ref index, out size), size)); + break; + case NtlmAttribute.Timestamp: + attributes.Add (new NtlmAttributeTimestampValuePair (attr, DecodeTimestamp (buffer, ref index, out size), size)); + break; + default: + attributes.Add (new NtlmAttributeByteArrayValuePair (attr, DecodeByteArray (buffer, ref index))); + break; + } + } while (index < startIndex + length); + } + + int CalculateSize (Encoding encoding) + { + int length = 4; + + foreach (var attribute in attributes) + length += attribute.GetEncodedLength (encoding); + + return length; + } + + /// + /// Encode the TargetInfo structure. + /// + /// + /// Encodes the TargetInfo structure. + /// + /// if the strings should be encoded in Unicode; otherwise, . + /// The encoded TargetInfo. + public byte[] Encode (bool unicode) + { + var encoding = unicode ? Encoding.Unicode : Encoding.UTF8; + var buf = new byte[CalculateSize (encoding)]; + int index = 0; + + foreach (var attribute in attributes) + attribute.EncodeTo (encoding, buf, ref index); + + return buf; + } + } +} diff --git a/MailKit/Security/Ntlm/NtlmUtils.cs b/MailKit/Security/Ntlm/NtlmUtils.cs new file mode 100644 index 0000000000..a2453e71c3 --- /dev/null +++ b/MailKit/Security/Ntlm/NtlmUtils.cs @@ -0,0 +1,253 @@ +// +// NtlmUtils.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/b38c36ed-2804-4868-a9ff-8dd3182128e4 + +using System; +using System.Text; +using System.Security.Cryptography; + +using SSCMD5 = System.Security.Cryptography.MD5; + +namespace MailKit.Security.Ntlm { + static class NtlmUtils + { + //static readonly byte[] ClientSealMagic = Encoding.ASCII.GetBytes ("session key to client-to-server sealing key magic constant"); + //static readonly byte[] ServerSealMagic = Encoding.ASCII.GetBytes ("session key to server-to-client sealing key magic constant"); + //static readonly byte[] ClientSignMagic = Encoding.ASCII.GetBytes ("session key to client-to-server signing key magic constant"); + //static readonly byte[] ServerSignMagic = Encoding.ASCII.GetBytes ("session key to server-to-client signing key magic constant"); + //static readonly byte[] SealKeySuffix40 = new byte[] { 0xe5, 0x38, 0xb0 }; + //static readonly byte[] SealKeySuffix56 = new byte[] { 0xa0 }; + static readonly byte[] Responserversion = new byte[] { 1 }; + static readonly byte[] HiResponserversion = new byte[] { 1 }; + static readonly byte[] Z0 = Array.Empty (); + static readonly byte[] Z24 = new byte[24]; + static readonly byte[] Z6 = new byte[6]; + static readonly byte[] Z4 = new byte[4]; + static readonly byte[] Z1 = new byte[1]; + + public static byte[] ConcatenationOf (params string[] values) + { + var concatenatedValue = string.Concat (values); + + return Encoding.Unicode.GetBytes (concatenatedValue); + } + + public static byte[] ConcatenationOf (params byte[][] values) + { + int index = 0, length = 0; + + for (int i = 0; i < values.Length; i++) + length += values[i].Length; + + var concatenated = new byte[length]; + for (int i = 0; i < values.Length; i++) { + length = values[i].Length; + Buffer.BlockCopy (values[i], 0, concatenated, index, length); + index += length; + } + + return concatenated; + } + + static byte[] MD4 (byte[] buffer) + { + using (var md4 = new MD4 ()) + return md4.ComputeHash (buffer); + } + + static byte[] MD4 (string password) + { + var unicode = Encoding.Unicode.GetBytes (password); + var hash = MD4 (unicode); + + Array.Clear (unicode, 0, unicode.Length); + + return hash; + } + + public static byte[] MD5 (byte[] buffer) + { + using (var md5 = SSCMD5.Create ()) + return md5.ComputeHash (buffer); + } + + public static byte[] HMACMD5 (byte[] key, params byte[][] values) + { + using (var md5 = new HMACMD5 (key)) { + int i; + + for (i = 0; i < values.Length - 1; i++) + md5.TransformBlock (values[i], 0, values[i].Length, null, 0); + + md5.TransformFinalBlock (values[i], 0, values[i].Length); + + return md5.Hash!; + } + } + + public static byte[] NONCE (int size) + { + var nonce = new byte[size]; + + using (var rng = RandomNumberGenerator.Create ()) + rng.GetBytes (nonce); + + return nonce; + } + + public static byte[] RC4K (byte[] key, byte[] message) + { + try { + using (var rc4 = new RC4 ()) { + rc4.Key = key; + + return rc4.TransformFinalBlock (message, 0, message.Length); + } + } finally { + Array.Clear (key, 0, key.Length); + } + } + +#if false + public static byte[] SEALKEY (NtlmFlags flags, byte[] exportedSessionKey, bool client = true) + { + if ((flags & NtlmFlags.NegotiateExtendedSessionSecurity) != 0) { + byte[] subkey; + + if ((flags & NtlmFlags.Negotiate128) != 0) { + subkey = exportedSessionKey; + } else if ((flags & NtlmFlags.Negotiate56) != 0) { + subkey = new byte[7]; + Buffer.BlockCopy (exportedSessionKey, 0, subkey, 0, subkey.Length); + } else { + subkey = new byte[5]; + Buffer.BlockCopy (exportedSessionKey, 0, subkey, 0, subkey.Length); + } + + var magic = client ? ClientSealMagic : ServerSealMagic; + var sealKey = MD5 (ConcatenationOf (subkey, magic)); + + if (subkey != exportedSessionKey) + Array.Clear (subkey, 0, subkey.Length); + + return sealKey; + } else if ((flags & NtlmFlags.NegotiateLanManagerKey) != 0) { + byte[] suffix; + int length; + + if ((flags & NtlmFlags.Negotiate56) != 0) { + suffix = SealKeySuffix56; + length = 7; + } else { + suffix = SealKeySuffix40; + length = 5; + } + + var sealKey = new byte[length + suffix.Length]; + Buffer.BlockCopy (exportedSessionKey, 0, sealKey, 0, length); + Buffer.BlockCopy (suffix, 0, sealKey, length, suffix.Length); + + return sealKey; + } else { + return exportedSessionKey; + } + } +#endif + +#if false + public static byte[] SIGNKEY (NtlmFlags flags, byte[] exportedSessionKey, bool client = true) + { + if ((flags & NtlmFlags.NegotiateExtendedSessionSecurity) != 0) { + var magic = client ? ClientSignMagic : ServerSignMagic; + return MD5 (ConcatenationOf (exportedSessionKey, magic)); + } else { + return null; + } + } +#endif + + static byte[] NTOWFv2 (string domain, string userName, string password) + { + var hash = MD4 (password); + byte[] responseKey; + + using (var md5 = new HMACMD5 (hash)) { + var userDom = ConcatenationOf (userName.ToUpperInvariant (), domain); + responseKey = md5.ComputeHash (userDom); + } + + Array.Clear (hash, 0, hash.Length); + + return responseKey; + } + + public static void ComputeNtlmV2 (NtlmChallengeMessage type2, string domain, string userName, string password, byte[] targetInfo, byte[] clientChallenge, long? time, out byte[]? ntChallengeResponse, out byte[] lmChallengeResponse, out byte[] sessionBaseKey) + { + if (userName.Length == 0 && password.Length == 0) { + // Special case for anonymous authentication + ntChallengeResponse = null; + lmChallengeResponse = Z1; + sessionBaseKey = Z0; + return; + } + + var timestamp = (time ?? DateTime.UtcNow.Ticks) - 504911232000000000; + var responseKey = NTOWFv2 (domain, userName, password); + + // Note: If NTLM v2 authentication is used, the client SHOULD send the timestamp in the CHALLENGE_MESSAGE. + if (type2.TargetInfo?.Timestamp != null) + timestamp = type2.TargetInfo.Timestamp.Value; + + // Set temp to ConcatenationOf(Responserversion, HiResponserversion, Z(6), Time, ClientChallenge, Z(4), ServerName, Z(4)) + var temp = ConcatenationOf (Responserversion, HiResponserversion, Z6, BitConverterLE.GetBytes (timestamp), clientChallenge, Z4, targetInfo, Z4); + + // Set NTProofStr to HMAC_MD5(ResponseKeyNT, ConcatenationOf(CHALLENGE_MESSAGE.ServerChallenge, temp)) + var proof = HMACMD5 (responseKey, type2.ServerChallenge, temp); + + // Set SessionBaseKey to HMAC_MD5(ResponseKeyNT, NTProofStr) + sessionBaseKey = HMACMD5 (responseKey, proof); + + // Set NtChallengeResponse to ConcatenationOf(NTProofStr, temp) + ntChallengeResponse = ConcatenationOf (proof, temp); + Array.Clear (proof, 0, proof.Length); + Array.Clear (temp, 0, temp.Length); + + // Note: If NTLM v2 authentication is used and the CHALLENGE_MESSAGE TargetInfo field (section 2.2.1.2) has an + // MsvAvTimestamp present, the client SHOULD NOT send the LmChallengeResponse and SHOULD send Z(24) instead. + if (type2.TargetInfo?.Timestamp == null) { + // Set LmChallengeResponse to ConcatenationOf(HMAC_MD5(ResponseKeyLM, ConcatenationOf(CHALLENGE_MESSAGE.ServerChallenge, ClientChallenge)), ClientChallenge) + var hash = HMACMD5 (responseKey, type2.ServerChallenge, clientChallenge); + lmChallengeResponse = ConcatenationOf (hash, clientChallenge); + Array.Clear (hash, 0, hash.Length); + } else { + lmChallengeResponse = Z24; + } + + Array.Clear (responseKey, 0, responseKey.Length); + } + } +} diff --git a/MailKit/Security/Ntlm/RC4.cs b/MailKit/Security/Ntlm/RC4.cs new file mode 100644 index 0000000000..380cbd1c70 --- /dev/null +++ b/MailKit/Security/Ntlm/RC4.cs @@ -0,0 +1,204 @@ +// +// ARC4Managed.cs: Alleged RC4(tm) compatible symmetric stream cipher +// RC4 is a trademark of RSA Security +// + +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Security.Cryptography; + +namespace MailKit.Security.Ntlm { + // References: + // a. Usenet 1994 - RC4 Algorithm revealed + // http://www.qrst.de/html/dsds/rc4.htm + + class RC4 : SymmetricAlgorithm, ICryptoTransform + { + readonly byte[] state = new byte[256]; + byte[]? key; + byte x, y; + bool disposed; + + public RC4 () : base () + { + KeySizeValue = 64; + } + + ~RC4 () + { + Dispose (false); + } + + public bool CanReuseTransform { + get { return false; } + } + + public bool CanTransformMultipleBlocks { + get { return true; } + } + + public int InputBlockSize { + get { return 1; } + } + + public int OutputBlockSize { + get { return 1; } + } + + public override byte[] Key { + get { + if (key == null) + throw new InvalidOperationException (); + + return (byte[]) key.Clone (); + } + set { + if (value == null) + throw new ArgumentNullException (nameof (value)); + + if (value.Length == 0) + throw new ArgumentException ("Invalid key length.", nameof (value)); + + KeySizeValue = value.Length << 3; + key = (byte[]) value.Clone (); + KeySetup (key); + } + } + + public override ICryptoTransform CreateEncryptor (byte[] rgbKey, byte[]? rgvIV) + { + return new RC4 { Key = rgbKey }; + } + + public override ICryptoTransform CreateDecryptor (byte[] rgbKey, byte[]? rgvIV) + { + return new RC4 { Key = rgbKey }; + } + + public override void GenerateIV () + { + // not used for a stream cipher + IV = Array.Empty (); + } + + public override void GenerateKey () + { + key = new byte[KeySizeValue >> 3]; + using (var rng = RandomNumberGenerator.Create ()) + rng.GetBytes (key); + KeySetup (key); + } + + void KeySetup (byte[] keyData) + { + byte index1 = 0; + byte index2 = 0; + + for (int counter = 0; counter < 256; counter++) + state[counter] = (byte) counter; + + x = y = 0; + + for (int counter = 0; counter < 256; counter++) { + index2 = (byte) (keyData[index1] + state[counter] + index2); + // swap byte + byte tmp = state[counter]; + state[counter] = state[index2]; + state[index2] = tmp; + index1 = (byte) ((index1 + 1) % keyData.Length); + } + } + + static void CheckInput (byte[] inputBuffer, int inputOffset, int inputCount) + { + if (inputBuffer == null) + throw new ArgumentNullException (nameof (inputBuffer)); + + if (inputOffset < 0 || inputOffset > inputBuffer.Length) + throw new ArgumentOutOfRangeException (nameof (inputOffset)); + + if (inputCount < 0 || inputOffset > inputBuffer.Length - inputCount) + throw new ArgumentOutOfRangeException (nameof (inputCount)); + } + + public int TransformBlock (byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset) + { + CheckInput (inputBuffer, inputOffset, inputCount); + + // check output parameters + if (outputBuffer == null) + throw new ArgumentNullException (nameof (outputBuffer)); + + if (outputOffset < 0 || outputOffset > outputBuffer.Length - inputCount) + throw new ArgumentOutOfRangeException (nameof (outputOffset)); + + return InternalTransformBlock (inputBuffer, inputOffset, inputCount, outputBuffer, outputOffset); + } + + int InternalTransformBlock (byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset) + { + byte xorIndex; + + for (int counter = 0; counter < inputCount; counter++) { + x = (byte) (x + 1); + y = (byte) (state[x] + y); + + // swap byte + byte tmp = state[x]; + state[x] = state[y]; + state[y] = tmp; + + xorIndex = (byte) (state[x] + state[y]); + outputBuffer[outputOffset + counter] = (byte) (inputBuffer[inputOffset + counter] ^ state[xorIndex]); + } + return inputCount; + } + + public byte[] TransformFinalBlock (byte[] inputBuffer, int inputOffset, int inputCount) + { + CheckInput (inputBuffer, inputOffset, inputCount); + + var output = new byte[inputCount]; + InternalTransformBlock (inputBuffer, inputOffset, inputCount, output, 0); + return output; + } + + protected override void Dispose (bool disposing) + { + if (disposed) + return; + + x = y = 0; + + if (key != null) + Array.Clear (key, 0, key.Length); + + Array.Clear (state, 0, state.Length); + + if (disposing) + key = null; + + disposed = true; + } + } +} diff --git a/MailKit/Security/Ntlm/TargetInfo.cs b/MailKit/Security/Ntlm/TargetInfo.cs deleted file mode 100644 index 9aa13bfda7..0000000000 --- a/MailKit/Security/Ntlm/TargetInfo.cs +++ /dev/null @@ -1,265 +0,0 @@ -// -// TargetInfo.cs -// -// Author: Jeffrey Stedfast -// -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -// - -using System; -using System.Text; - -#if NETFX_CORE -using Encoding = Portable.Text.Encoding; -#endif - -namespace MailKit.Security.Ntlm { - class TargetInfo - { - public TargetInfo (byte[] buffer, int startIndex, int length, bool unicode) - { - Decode (buffer, startIndex, length, unicode); - } - - public TargetInfo () - { - } - - public int? Flags { - get; set; - } - - public string DomainName { - get; set; - } - - public string ServerName { - get; set; - } - - public string DnsDomainName { - get; set; - } - - public string DnsServerName { - get; set; - } - - public string DnsTreeName { - get; set; - } - - public string TargetName { - get; set; - } - - public long Timestamp { - get; set; - } - - static string DecodeString (byte[] buffer, ref int index, bool unicode) - { - var encoding = unicode ? Encoding.Unicode : Encoding.UTF8; - var length = BitConverterLE.ToInt16 (buffer, index); - var value = encoding.GetString (buffer, index + 2, length); - - index += 2 + length; - - return value; - } - - static int DecodeFlags (byte[] buffer, ref int index) - { - short nbytes = BitConverterLE.ToInt16 (buffer, index); - int flags; - - index += 2; - - switch (nbytes) { - case 4: flags = BitConverterLE.ToInt32 (buffer, index); break; - case 2: flags = BitConverterLE.ToInt16 (buffer, index); break; - default: flags = 0; break; - } - - index += nbytes; - - return flags; - } - - static long DecodeTimestamp (byte[] buffer, ref int index) - { - short nbytes = BitConverterLE.ToInt16 (buffer, index); - long lo, hi; - - index += 2; - - switch (nbytes) { - case 8: - lo = BitConverterLE.ToUInt32 (buffer, index); - index += 4; - hi = BitConverterLE.ToUInt32 (buffer, index); - index += 4; - return (hi << 32) | lo; - case 4: - lo = BitConverterLE.ToUInt32 (buffer, index); - index += 4; - return lo; - case 2: - lo = BitConverterLE.ToUInt16 (buffer, index); - index += 2; - return lo; - default: - index += nbytes; - return 0; - } - } - - void Decode (byte[] buffer, int startIndex, int length, bool unicode) - { - int index = startIndex; - - do { - var type = BitConverterLE.ToInt16 (buffer, index); - - index += 2; - - switch (type) { - case 0: index = startIndex + length; break; // a 'type' of 0 terminates the TargetInfo - case 1: ServerName = DecodeString (buffer, ref index, unicode); break; - case 2: DomainName = DecodeString (buffer, ref index, unicode); break; - case 3: DnsServerName = DecodeString (buffer, ref index, unicode); break; - case 4: DnsDomainName = DecodeString (buffer, ref index, unicode); break; - case 5: DnsTreeName = DecodeString (buffer, ref index, unicode); break; - case 6: Flags = DecodeFlags (buffer, ref index); break; - case 7: Timestamp = DecodeTimestamp (buffer, ref index); break; - case 9: TargetName = DecodeString (buffer, ref index, unicode); break; - default: index += 2 + BitConverterLE.ToInt16 (buffer, index); break; - } - } while (index < startIndex + length); - } - - int CalculateSize (bool unicode) - { - var encoding = unicode ? Encoding.Unicode : Encoding.UTF8; - int length = 4; - - if (!string.IsNullOrEmpty (DomainName)) - length += 4 + encoding.GetByteCount (DomainName); - - if (!string.IsNullOrEmpty (ServerName)) - length += 4 + encoding.GetByteCount (ServerName); - - if (!string.IsNullOrEmpty (DnsDomainName)) - length += 4 + encoding.GetByteCount (DnsDomainName); - - if (!string.IsNullOrEmpty (DnsServerName)) - length += 4 + encoding.GetByteCount (DnsServerName); - - if (!string.IsNullOrEmpty (DnsTreeName)) - length += 4 + encoding.GetByteCount (DnsTreeName); - - if (Flags.HasValue) - length += 8; - - if (Timestamp != 0) - length += 12; - - if (!string.IsNullOrEmpty (TargetName)) - length += 4 + encoding.GetByteCount (TargetName); - - return length; - } - - static void EncodeTypeAndLength (byte[] buf, ref int index, short type, short length) - { - buf[index++] = (byte) (type); - buf[index++] = (byte) (type >> 8); - buf[index++] = (byte) (length); - buf[index++] = (byte) (length >> 8); - } - - static void EncodeString (byte[] buf, ref int index, short type, string value, bool unicode) - { - var encoding = unicode ? Encoding.Unicode : Encoding.UTF8; - int length = value.Length; - - if (unicode) - length *= 2; - - EncodeTypeAndLength (buf, ref index, type, (short) length); - encoding.GetBytes (value, 0, value.Length, buf, index); - index += length; - } - - static void EncodeInt32 (byte[] buf, ref int index, int value) - { - buf[index++] = (byte) (value); - buf[index++] = (byte) (value >> 8); - buf[index++] = (byte) (value >> 16); - buf[index++] = (byte) (value >> 24); - } - - static void EncodeTimestamp (byte[] buf, ref int index, short type, long value) - { - EncodeTypeAndLength (buf, ref index, type, 8); - EncodeInt32 (buf, ref index, (int) (value & 0xffffffff)); - EncodeInt32 (buf, ref index, (int) (value >> 32)); - } - - static void EncodeFlags (byte[] buf, ref int index, short type, int value) - { - EncodeTypeAndLength (buf, ref index, type, 4); - EncodeInt32 (buf, ref index, value); - } - - public byte[] Encode (bool unicode) - { - var buf = new byte[CalculateSize (unicode)]; - int index = 0; - - if (!string.IsNullOrEmpty (DomainName)) - EncodeString (buf, ref index, 2, DomainName, unicode); - - if (!string.IsNullOrEmpty (ServerName)) - EncodeString (buf, ref index, 1, ServerName, unicode); - - if (!string.IsNullOrEmpty (DnsDomainName)) - EncodeString (buf, ref index, 4, DnsDomainName, unicode); - - if (!string.IsNullOrEmpty (DnsServerName)) - EncodeString (buf, ref index, 3, DnsServerName, unicode); - - if (!string.IsNullOrEmpty (DnsTreeName)) - EncodeString (buf, ref index, 5, DnsTreeName, unicode); - - if (Flags.HasValue) - EncodeFlags (buf, ref index, 6, Flags.Value); - - if (Timestamp != 0) - EncodeTimestamp (buf, ref index, 7, Timestamp); - - if (!string.IsNullOrEmpty (TargetName)) - EncodeString (buf, ref index, 9, TargetName, unicode); - - return buf; - } - } -} diff --git a/MailKit/Security/Ntlm/Type1Message.cs b/MailKit/Security/Ntlm/Type1Message.cs deleted file mode 100644 index f09295390e..0000000000 --- a/MailKit/Security/Ntlm/Type1Message.cs +++ /dev/null @@ -1,171 +0,0 @@ -// -// Mono.Security.Protocol.Ntlm.Type1Message - Negotiation -// -// Authors: Sebastien Pouliot -// Jeffrey Stedfast -// -// Copyright (c) 2003 Motus Technologies Inc. (http://www.motus.com) -// Copyright (c) 2004 Novell, Inc (http://www.novell.com) -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) -// -// References -// a. NTLM Authentication Scheme for HTTP, Ronald Tschalär -// http://www.innovation.ch/java/ntlm.html -// b. The NTLM Authentication Protocol, Copyright © 2003 Eric Glass -// http://davenport.sourceforge.net/ntlm.html -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// - -using System; -using System.Text; - -#if NETFX_CORE -using Encoding = Portable.Text.Encoding; -#endif - -namespace MailKit.Security.Ntlm { - class Type1Message : MessageBase - { - internal static readonly NtlmFlags DefaultFlags = NtlmFlags.NegotiateNtlm | NtlmFlags.NegotiateOem | NtlmFlags.NegotiateUnicode | NtlmFlags.RequestTarget; - - string domain; - string host; - - public Type1Message (string hostName, string domainName) : base (1) - { - Flags = DefaultFlags; - Domain = domainName; - Host = hostName; - } - - public Type1Message (byte[] message, int startIndex, int length) : base (1) - { - Decode (message, startIndex, length); - } - - public string Domain { - get { return domain; } - set { - if (string.IsNullOrEmpty (value)) { - Flags &= ~NtlmFlags.NegotiateDomainSupplied; - value = string.Empty; - } else { - Flags |= NtlmFlags.NegotiateDomainSupplied; - } - - domain = value; - } - } - - public string Host { - get { return host; } - set { - if (string.IsNullOrEmpty (value)) { - Flags &= ~NtlmFlags.NegotiateWorkstationSupplied; - value = string.Empty; - } else { - Flags |= NtlmFlags.NegotiateWorkstationSupplied; - } - - host = value; - } - } - - public Version OSVersion { - get; set; - } - - void Decode (byte[] message, int startIndex, int length) - { - int offset, count; - - ValidateArguments (message, startIndex, length); - - Flags = (NtlmFlags) BitConverterLE.ToUInt32 (message, startIndex + 12); - - // decode the domain - count = BitConverterLE.ToUInt16 (message, startIndex + 16); - offset = BitConverterLE.ToUInt16 (message, startIndex + 20); - domain = Encoding.UTF8.GetString (message, startIndex + offset, count); - - // decode the workstation/host - count = BitConverterLE.ToUInt16 (message, startIndex + 24); - offset = BitConverterLE.ToUInt16 (message, startIndex + 28); - host = Encoding.UTF8.GetString (message, startIndex + offset, count); - - if (offset == 40) { - // decode the OS Version - int major = message[startIndex + 32]; - int minor = message[startIndex + 33]; - int build = BitConverterLE.ToUInt16 (message, startIndex + 34); - - OSVersion = new Version (major, minor, build); - } - } - - public override byte[] Encode () - { - int versionLength = OSVersion != null ? 8 : 0; - int hostOffset = 32 + versionLength; - int domainOffset = hostOffset + host.Length; - - var message = PrepareMessage (32 + domain.Length + host.Length + versionLength); - - message[12] = (byte) Flags; - message[13] = (byte)((uint) Flags >> 8); - message[14] = (byte)((uint) Flags >> 16); - message[15] = (byte)((uint) Flags >> 24); - - message[16] = (byte) domain.Length; - message[17] = (byte)(domain.Length >> 8); - message[18] = message[16]; - message[19] = message[17]; - message[20] = (byte) domainOffset; - message[21] = (byte)(domainOffset >> 8); - - message[24] = (byte) host.Length; - message[25] = (byte)(host.Length >> 8); - message[26] = message[24]; - message[27] = message[25]; - message[28] = (byte) hostOffset; - message[29] = (byte)(hostOffset >> 8); - - if (OSVersion != null) { - message[32] = (byte) OSVersion.Major; - message[33] = (byte) OSVersion.Minor; - message[34] = (byte)(OSVersion.Build); - message[35] = (byte)(OSVersion.Build >> 8); - message[36] = 0x00; - message[37] = 0x00; - message[38] = 0x00; - message[39] = 0x0f; - } - - var hostName = Encoding.UTF8.GetBytes (host.ToUpperInvariant ()); - Buffer.BlockCopy (hostName, 0, message, hostOffset, hostName.Length); - - var domainName = Encoding.UTF8.GetBytes (domain.ToUpperInvariant ()); - Buffer.BlockCopy (domainName, 0, message, domainOffset, domainName.Length); - - return message; - } - } -} diff --git a/MailKit/Security/Ntlm/Type2Message.cs b/MailKit/Security/Ntlm/Type2Message.cs deleted file mode 100644 index 83a22a2e67..0000000000 --- a/MailKit/Security/Ntlm/Type2Message.cs +++ /dev/null @@ -1,154 +0,0 @@ -// -// Mono.Security.Protocol.Ntlm.Type2Message - Challenge -// -// Authors: Sebastien Pouliot -// Jeffrey Stedfast -// -// Copyright (c) 2003 Motus Technologies Inc. (http://www.motus.com) -// Copyright (c) 2004 Novell, Inc (http://www.novell.com) -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) -// -// References -// a. NTLM Authentication Scheme for HTTP, Ronald Tschalär -// http://www.innovation.ch/java/ntlm.html -// b. The NTLM Authentication Protocol, Copyright © 2003 Eric Glass -// http://davenport.sourceforge.net/ntlm.html -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// - -using System; -using System.Text; - -#if !NETFX_CORE -using System.Security.Cryptography; -#else -using Encoding = Portable.Text.Encoding; -#endif - -namespace MailKit.Security.Ntlm { - class Type2Message : MessageBase - { - byte[] targetInfo; - byte[] nonce; - - public Type2Message () : base (2) - { - Flags = (NtlmFlags) 0x8201; - nonce = new byte[8]; - - using (var rng = RandomNumberGenerator.Create ()) - rng.GetBytes (nonce); - } - - public Type2Message (byte[] message, int startIndex, int length) : base (2) - { - nonce = new byte[8]; - Decode (message, startIndex, length); - } - - ~Type2Message () - { - if (nonce != null) - Array.Clear (nonce, 0, nonce.Length); - } - - public byte[] Nonce { - get { return (byte[]) nonce.Clone (); } - set { - if (value == null) - throw new ArgumentNullException (nameof (value)); - - if (value.Length != 8) - throw new ArgumentException ("Invalid Nonce Length (should be 8 bytes).", nameof (value)); - - nonce = (byte[]) value.Clone (); - } - } - - public string TargetName { - get; private set; - } - - public TargetInfo TargetInfo { - get; private set; - } - - public byte[] EncodedTargetInfo { - get { - if (targetInfo != null) - return (byte[]) targetInfo.Clone (); - - return new byte[0]; - } - } - - void Decode (byte[] message, int startIndex, int length) - { - ValidateArguments (message, startIndex, length); - - Flags = (NtlmFlags) BitConverterLE.ToUInt32 (message, startIndex + 20); - - Buffer.BlockCopy (message, startIndex + 24, nonce, 0, 8); - - var targetNameLength = BitConverterLE.ToUInt16 (message, startIndex + 12); - var targetNameOffset = BitConverterLE.ToUInt16 (message, startIndex + 16); - - if (targetNameLength > 0) { - var encoding = (Flags & NtlmFlags.NegotiateOem) != 0 ? Encoding.UTF8 : Encoding.Unicode; - - TargetName = encoding.GetString (message, startIndex + targetNameOffset, targetNameLength); - } - - // The Target Info block is optional. - if (message.Length >= 48 && targetNameOffset >= 48) { - var targetInfoLength = BitConverterLE.ToUInt16 (message, startIndex + 40); - var targetInfoOffset = BitConverterLE.ToUInt16 (message, startIndex + 44); - - if (targetInfoLength > 0 && targetInfoOffset < message.Length && targetInfoLength <= (message.Length - targetInfoOffset)) { - TargetInfo = new TargetInfo (message, startIndex + targetInfoOffset, targetInfoLength, (Flags & NtlmFlags.NegotiateUnicode) != 0); - - targetInfo = new byte[targetInfoLength]; - Buffer.BlockCopy (message, startIndex + targetInfoOffset, targetInfo, 0, targetInfoLength); - } - } - } - - public override byte[] Encode () - { - byte[] data = PrepareMessage (40); - - // message length - short length = (short) data.Length; - data[16] = (byte) length; - data[17] = (byte)(length >> 8); - - // flags - data[20] = (byte) Flags; - data[21] = (byte)((uint) Flags >> 8); - data[22] = (byte)((uint) Flags >> 16); - data[23] = (byte)((uint) Flags >> 24); - - Buffer.BlockCopy (nonce, 0, data, 24, nonce.Length); - - return data; - } - } -} diff --git a/MailKit/Security/Ntlm/Type3Message.cs b/MailKit/Security/Ntlm/Type3Message.cs deleted file mode 100644 index ec02b5731b..0000000000 --- a/MailKit/Security/Ntlm/Type3Message.cs +++ /dev/null @@ -1,319 +0,0 @@ -// -// Mono.Security.Protocol.Ntlm.Type3Message - Authentication -// -// Authors: Sebastien Pouliot -// Jeffrey Stedfast -// -// Copyright (c) 2003 Motus Technologies Inc. (http://www.motus.com) -// Copyright (c) 2004 Novell, Inc (http://www.novell.com) -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) -// -// References -// a. NTLM Authentication Scheme for HTTP, Ronald Tschalär -// http://www.innovation.ch/java/ntlm.html -// b. The NTLM Authentication Protocol, Copyright © 2003 Eric Glass -// http://davenport.sourceforge.net/ntlm.html -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// - -using System; -using System.Text; - -#if NETFX_CORE -using Encoding = Portable.Text.Encoding; -#endif - -namespace MailKit.Security.Ntlm { - class Type3Message : MessageBase - { - readonly Type2Message type2; - readonly byte[] challenge; - string domain; - string host; - - public Type3Message (byte[] message, int startIndex, int length) : base (3) - { - Decode (message, startIndex, length); - type2 = null; - } - - public Type3Message (Type2Message type2, string userName, string hostName) : base (3) - { - this.type2 = type2; - - Level = NtlmSettings.DefaultAuthLevel; - - challenge = (byte[]) type2.Nonce.Clone (); - domain = type2.TargetName; - Username = userName; - host = hostName; - - Flags = (NtlmFlags) 0x8200; - if ((type2.Flags & NtlmFlags.NegotiateUnicode) != 0) - Flags |= NtlmFlags.NegotiateUnicode; - else - Flags |= NtlmFlags.NegotiateOem; - - if ((type2.Flags & NtlmFlags.NegotiateNtlm2Key) != 0) - Flags |= NtlmFlags.NegotiateNtlm2Key; - - if ((type2.Flags & NtlmFlags.NegotiateVersion) != 0) - Flags |= NtlmFlags.NegotiateVersion; - } - - ~Type3Message () - { - if (challenge != null) - Array.Clear (challenge, 0, challenge.Length); - - if (LM != null) - Array.Clear (LM, 0, LM.Length); - - if (NT != null) - Array.Clear (NT, 0, NT.Length); - } - - public NtlmAuthLevel Level { - get; set; - } - - public string Domain { - get { return domain; } - set { - if (string.IsNullOrEmpty (value)) { - Flags &= ~NtlmFlags.NegotiateDomainSupplied; - value = string.Empty; - } else { - Flags |= NtlmFlags.NegotiateDomainSupplied; - } - - domain = value; - } - } - - public string Host { - get { return host; } - set { - if (string.IsNullOrEmpty (value)) { - Flags &= ~NtlmFlags.NegotiateWorkstationSupplied; - value = string.Empty; - } else { - Flags |= NtlmFlags.NegotiateWorkstationSupplied; - } - - host = value; - } - } - - public string Password { - get; set; - } - - public string Username { - get; set; - } - - public byte[] LM { - get; private set; - } - - public byte[] NT { - get; set; - } - - void Decode (byte[] message, int startIndex, int length) - { - ValidateArguments (message, startIndex, length); - - Password = null; - - if (message.Length >= 64) - Flags = (NtlmFlags) BitConverterLE.ToUInt32 (message, startIndex + 60); - else - Flags = (NtlmFlags) 0x8201; - - int lmLength = BitConverterLE.ToUInt16 (message, startIndex + 12); - int lmOffset = BitConverterLE.ToUInt16 (message, startIndex + 16); - LM = new byte[lmLength]; - Buffer.BlockCopy (message, startIndex + lmOffset, LM, 0, lmLength); - - int ntLength = BitConverterLE.ToUInt16 (message, startIndex + 20); - int ntOffset = BitConverterLE.ToUInt16 (message, startIndex + 24); - NT = new byte[ntLength]; - Buffer.BlockCopy (message, startIndex + ntOffset, NT, 0, ntLength); - - int domainLength = BitConverterLE.ToUInt16 (message, startIndex + 28); - int domainOffset = BitConverterLE.ToUInt16 (message, startIndex + 32); - domain = DecodeString (message, startIndex + domainOffset, domainLength); - - int userLength = BitConverterLE.ToUInt16 (message, startIndex + 36); - int userOffset = BitConverterLE.ToUInt16 (message, startIndex + 40); - Username = DecodeString (message, startIndex + userOffset, userLength); - - int hostLength = BitConverterLE.ToUInt16 (message, startIndex + 44); - int hostOffset = BitConverterLE.ToUInt16 (message, startIndex + 48); - host = DecodeString (message, startIndex + hostOffset, hostLength); - - // Session key. We don't use it yet. - // int skeyLength = BitConverterLE.ToUInt16 (message, startIndex + 52); - // int skeyOffset = BitConverterLE.ToUInt16 (message, startIndex + 56); - } - - string DecodeString (byte[] buffer, int offset, int len) - { - var encoding = (Flags & NtlmFlags.NegotiateUnicode) != 0 ? Encoding.Unicode : Encoding.UTF8; - - return encoding.GetString (buffer, offset, len); - } - - byte[] EncodeString (string text) - { - if (text == null) - return new byte[0]; - - var encoding = (Flags & NtlmFlags.NegotiateUnicode) != 0 ? Encoding.Unicode : Encoding.UTF8; - - return encoding.GetBytes (text); - } - - public override byte[] Encode () - { - var target = EncodeString (domain); - var user = EncodeString (Username); - var hostName = EncodeString (host); - var payloadOffset = 64; - bool reqVersion; - byte[] lm, ntlm; - - if (type2 == null) { - if (Level != NtlmAuthLevel.LM_and_NTLM) - throw new InvalidOperationException ("Refusing to use legacy-mode LM/NTLM authentication unless explicitly enabled using NtlmSettings.DefaultAuthLevel."); - - using (var legacy = new ChallengeResponse (Password, challenge)) { - lm = legacy.LM; - ntlm = legacy.NT; - } - - reqVersion = false; - } else { - ChallengeResponse2.Compute (type2, Level, Username, Password, domain, out lm, out ntlm); - - if ((reqVersion = (type2.Flags & NtlmFlags.NegotiateVersion) != 0)) - payloadOffset += 8; - } - - var lmResponseLength = lm != null ? lm.Length : 0; - var ntResponseLength = ntlm != null ? ntlm.Length : 0; - - var data = PrepareMessage (payloadOffset + target.Length + user.Length + hostName.Length + lmResponseLength + ntResponseLength); - - // LM response - short lmResponseOffset = (short) (payloadOffset + target.Length + user.Length + hostName.Length); - data[12] = (byte) lmResponseLength; - data[13] = (byte) 0x00; - data[14] = data[12]; - data[15] = data[13]; - data[16] = (byte) lmResponseOffset; - data[17] = (byte) (lmResponseOffset >> 8); - - // NT response - short ntResponseOffset = (short) (lmResponseOffset + lmResponseLength); - data[20] = (byte) ntResponseLength; - data[21] = (byte) (ntResponseLength >> 8); - data[22] = data[20]; - data[23] = data[21]; - data[24] = (byte) ntResponseOffset; - data[25] = (byte) (ntResponseOffset >> 8); - - // target - short domainLength = (short) target.Length; - short domainOffset = (short) payloadOffset; - data[28] = (byte) domainLength; - data[29] = (byte) (domainLength >> 8); - data[30] = data[28]; - data[31] = data[29]; - data[32] = (byte) domainOffset; - data[33] = (byte) (domainOffset >> 8); - - // username - short userLength = (short) user.Length; - short userOffset = (short) (domainOffset + domainLength); - data[36] = (byte) userLength; - data[37] = (byte) (userLength >> 8); - data[38] = data[36]; - data[39] = data[37]; - data[40] = (byte) userOffset; - data[41] = (byte) (userOffset >> 8); - - // host - short hostLength = (short) hostName.Length; - short hostOffset = (short) (userOffset + userLength); - data[44] = (byte) hostLength; - data[45] = (byte) (hostLength >> 8); - data[46] = data[44]; - data[47] = data[45]; - data[48] = (byte) hostOffset; - data[49] = (byte) (hostOffset >> 8); - - // message length - short messageLength = (short) data.Length; - data[56] = (byte) messageLength; - data[57] = (byte) (messageLength >> 8); - - // options flags - data[60] = (byte) Flags; - data[61] = (byte)((uint) Flags >> 8); - data[62] = (byte)((uint) Flags >> 16); - data[63] = (byte)((uint) Flags >> 24); - - if (reqVersion) { - // encode the Windows version as Windows 10.0 - data[64] = 0x0A; - data[65] = 0x0; - - // encode the ProductBuild version - data[66] = (byte) (10586 & 0xff); - data[67] = (byte) (10586 >> 8); - - // next 3 bytes are reserved and should remain 0 - - // encode the NTLMRevisionCurrent version - data[71] = 0x0F; - } - - Buffer.BlockCopy (target, 0, data, domainOffset, target.Length); - Buffer.BlockCopy (user, 0, data, userOffset, user.Length); - Buffer.BlockCopy (hostName, 0, data, hostOffset, hostName.Length); - - if (lm != null) { - Buffer.BlockCopy (lm, 0, data, lmResponseOffset, lm.Length); - Array.Clear (lm, 0, lm.Length); - } - - if (ntlm != null) { - Buffer.BlockCopy (ntlm, 0, data, ntResponseOffset, ntlm.Length); - Array.Clear (ntlm, 0, ntlm.Length); - } - - return data; - } - } -} diff --git a/MailKit/Security/RandomNumberGenerator.cs b/MailKit/Security/RandomNumberGenerator.cs index 6fda33cfc6..eee2d5c984 100644 --- a/MailKit/Security/RandomNumberGenerator.cs +++ b/MailKit/Security/RandomNumberGenerator.cs @@ -3,7 +3,7 @@ // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2019 Xamarin Inc. (www.xamarin.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal diff --git a/MailKit/Security/SaslException.cs b/MailKit/Security/SaslException.cs index 730484aa3d..5870ba48b7 100644 --- a/MailKit/Security/SaslException.cs +++ b/MailKit/Security/SaslException.cs @@ -1,9 +1,9 @@ -// +// // SaslException.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -82,6 +82,7 @@ public class SaslException : AuthenticationException /// /// The serialization info. /// The streaming context. + [Obsolete ("This API supports obsolete formatter-based serialization. It should not be called or extended by application code.")] protected SaslException (SerializationInfo info, StreamingContext context) : base (info, context) { ErrorCode = (SaslErrorCode) info.GetInt32 ("ErrorCode"); @@ -99,7 +100,7 @@ protected SaslException (SerializationInfo info, StreamingContext context) : bas /// The error code. /// The error message. /// - /// is null. + /// is . /// public SaslException (string mechanism, SaslErrorCode code, string message) : base (message) { @@ -121,18 +122,18 @@ public SaslException (string mechanism, SaslErrorCode code, string message) : ba /// The serialization info. /// The streaming context. /// - /// is null. + /// is . /// [SecurityCritical] +#if NET8_0_OR_GREATER + [Obsolete ("This API supports obsolete formatter-based serialization. It should not be called or extended by application code.")] +#endif public override void GetObjectData (SerializationInfo info, StreamingContext context) { - if (info == null) - throw new ArgumentNullException (nameof (info)); + base.GetObjectData (info, context); info.AddValue ("ErrorCode", (int) ErrorCode); info.AddValue ("Mechanism", Mechanism); - - base.GetObjectData (info, context); } #endif diff --git a/MailKit/Security/SaslMechanism.cs b/MailKit/Security/SaslMechanism.cs index 27096495c1..7735067af2 100644 --- a/MailKit/Security/SaslMechanism.cs +++ b/MailKit/Security/SaslMechanism.cs @@ -1,9 +1,9 @@ -// +// // SaslMechanism.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -27,10 +27,14 @@ using System; using System.Net; using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Security.Cryptography; +using System.Diagnostics.CodeAnalysis; +using System.Security.Authentication.ExtendedProtection; -#if NETFX_CORE -using Encoding = Portable.Text.Encoding; -#endif +using MailKit.Net; namespace MailKit.Security { /// @@ -40,7 +44,7 @@ namespace MailKit.Security { /// Authenticating via a SASL mechanism may be a multi-step process. /// To determine if the mechanism has completed the necessary steps /// to authentication, check the after - /// each call to . + /// each call to . /// public abstract class SaslMechanism { @@ -48,12 +52,86 @@ public abstract class SaslMechanism /// The supported authentication mechanisms in order of strongest to weakest. /// /// - /// Used by the various clients when authenticating via SASL to determine - /// which order the SASL mechanisms supported by the server should be tried. + /// Used by the various clients when authenticating via SASL to determine + /// which order the SASL mechanisms supported by the server should be tried. /// - public static readonly string[] AuthMechanismRank = { - "XOAUTH2", "SCRAM-SHA-256", "SCRAM-SHA-1", "NTLM", "CRAM-MD5", "DIGEST-MD5", "PLAIN", "LOGIN" - }; + static readonly string[] RankedAuthenticationMechanisms; + +#if NET7_0_OR_GREATER + static readonly Lazy NativeNtlmSupported = new Lazy (CheckNativeNtlmSupported, LazyThreadSafetyMode.ExecutionAndPublication); + static readonly Lazy GssapiSupported = new Lazy (CheckGssapiSupported, LazyThreadSafetyMode.ExecutionAndPublication); +#endif + static readonly bool Md5Supported; + + static SaslMechanism () + { + try { + using (var md5 = MD5.Create ()) + Md5Supported = true; + } catch { + Md5Supported = false; + } + + var supported = new List { + "SCRAM-SHA-512", + "SCRAM-SHA-256", + "SCRAM-SHA-1", + + // Note: NTLM is considered less secure than even SCRAM-SHA-1 (even though SHA-1 is considered weak) + // because it is vulnerable to replay and MitM attacks. The cryptography used by NTLM is also very + // weak at this point (DES for NTLMv1 and HMAC-MD5 for NTLMv2). + "NTLM" + }; + + if (Md5Supported) { + supported.Add ("DIGEST-MD5"); + supported.Add ("CRAM-MD5"); + } + supported.Add ("PLAIN"); + supported.Add ("LOGIN"); + + RankedAuthenticationMechanisms = supported.ToArray (); + } + +#if NET7_0_OR_GREATER + static bool CheckNativeNtlmSupported () + { + return SaslMechanismNegotiateBase.CheckSupported ("NTLM"); + } + + static bool CheckGssapiSupported () + { + return SaslMechanismNegotiateBase.CheckSupported ("GSSAPI"); + } +#endif + + /// + /// Rank authentication mechanisms in order of security. + /// + /// + /// Ranks authentication mechanisms in order of security. + /// + /// The authentication mechanisms supported by the server. + /// The supported authentication mechanisms in ranked order. + internal static IEnumerable Rank (HashSet authenticationMechanisms) + { + foreach (var mechanism in RankedAuthenticationMechanisms) { + if (mechanism.StartsWith ("SCRAM-SHA", StringComparison.Ordinal)) { + var plus = mechanism + "-PLUS"; + + if (authenticationMechanisms.Contains (plus)) { + // Note: If the server supports SCRAM-SHA-#-PLUS, we opt for the -PLUS variant and do not include the non-PLUS variant. + yield return plus; + continue; + } + } + + if (authenticationMechanisms.Contains (mechanism)) + yield return mechanism; + } + + yield break; + } /// /// Initializes a new instance of the class. @@ -61,30 +139,47 @@ public abstract class SaslMechanism /// /// Creates a new SASL context. /// - /// The URI of the service. /// The user's credentials. /// - /// is null. - /// -or- - /// is null. + /// is . /// - protected SaslMechanism (Uri uri, ICredentials credentials) + protected SaslMechanism (NetworkCredential credentials) { - if (uri == null) - throw new ArgumentNullException (nameof (uri)); - if (credentials == null) throw new ArgumentNullException (nameof (credentials)); Credentials = credentials; - Uri = uri; } /// - /// Gets the name of the mechanism. + /// Initializes a new instance of the class. /// /// - /// Gets the name of the mechanism. + /// Creates a new SASL context. + /// + /// The user name. + /// The password. + /// + /// is . + /// -or- + /// is . + /// + protected SaslMechanism (string userName, string password) + { + if (userName == null) + throw new ArgumentNullException (nameof (userName)); + + if (password == null) + throw new ArgumentNullException (nameof (password)); + + Credentials = new NetworkCredential (userName, password); + } + + /// + /// Get the name of the SASL mechanism. + /// + /// + /// Gets the name of the SASL mechanism. /// /// The name of the mechanism. public abstract string MechanismName { @@ -92,65 +187,166 @@ public abstract string MechanismName { } /// - /// Gets the user's credentials. + /// Get the user's credentials. /// /// /// Gets the user's credentials. /// /// The user's credentials. - public ICredentials Credentials { + public NetworkCredential Credentials { get; private set; } /// - /// Gets whether or not the mechanism supports an initial response (SASL-IR). + /// Get whether or not the SASL mechanism supports channel binding. /// /// - /// SASL mechanisms that support sending an initial client response to the server - /// should return true. + /// Gets whether or not the SASL mechanism supports channel binding. /// - /// true if the mechanism supports an initial response; otherwise, false. + /// if the SASL mechanism supports channel binding; otherwise, . + public virtual bool SupportsChannelBinding { + get { return false; } + } + + /// + /// Get whether or not the SASL mechanism supports an initial response (SASL-IR). + /// + /// + /// Gets whether or not the SASL mechanism supports an initial response (SASL-IR). + /// SASL mechanisms that support sending an initial client response to the server + /// should return . + /// + /// if the SASL mechanism supports an initial response; otherwise, . public virtual bool SupportsInitialResponse { get { return false; } } /// - /// Gets or sets whether the SASL mechanism has finished authenticating. + /// Get or set whether the SASL mechanism has finished authenticating. /// /// /// Gets or sets whether the SASL mechanism has finished authenticating. /// - /// true if the SASL mechanism has finished authenticating; otherwise, false. + /// if the SASL mechanism has finished authenticating; otherwise, . public bool IsAuthenticated { get; protected set; } /// - /// Gets whether or not a security layer was negotiated. + /// Get whether or not channel-binding was negotiated by the SASL mechanism. + /// + /// + /// Gets whether or not channel-binding has been negotiated by the SASL mechanism. + /// Some SASL mechanisms, such as SCRAM-SHA1-PLUS and NTLM, are able to negotiate + /// channel-bindings. + /// + /// if channel-binding was negotiated; otherwise, . + public virtual bool NegotiatedChannelBinding { + get { return false; } + } + + /// + /// Get whether or not a security layer was negotiated by the SASL mechanism. /// /// /// Gets whether or not a security layer has been negotiated by the SASL mechanism. /// Some SASL mechanisms, such as GSSAPI, are able to negotiate security layers /// such as integrity and confidentiality protection. /// - /// true if a security layer was negotiated; otherwise, false. + /// if a security layer was negotiated; otherwise, . public virtual bool NegotiatedSecurityLayer { get { return false; } } /// - /// Gets or sets the URI of the service. + /// Get or set the channel-binding context. + /// + /// + /// Gets or sets the channel-binding context. + /// + /// The channel-binding context. + internal IChannelBindingContext? ChannelBindingContext { + get; set; + } + + /// + /// Get or set the URI of the service. /// /// /// Gets or sets the URI of the service. /// /// The URI of the service. - public Uri Uri { - get; protected set; + internal Uri? Uri { + get; set; } +#if NET8_0_OR_GREATER /// - /// Parses the server's challenge token and returns the next challenge response. + /// Try to get a channel-binding. + /// + /// + /// Tries to get the specified channel-binding. + /// + /// The kind of channel-binding desired. + /// A buffer containing the channel-binding. + /// if the channel-binding token was acquired; otherwise, . + protected bool TryGetChannelBinding (ChannelBindingKind kind, [NotNullWhen (true)] out ChannelBinding? channelBinding) + { + if (ChannelBindingContext == null) { + channelBinding = null; + return false; + } + + return ChannelBindingContext.TryGetChannelBinding (kind, out channelBinding); + } +#endif + + /// + /// Try to get a channel-binding token. + /// + /// + /// Tries to get the specified channel-binding. + /// + /// The kind of channel-binding desired. + /// A buffer containing the channel-binding token. + /// if the channel-binding token was acquired; otherwise, . + protected bool TryGetChannelBindingToken (ChannelBindingKind kind, [NotNullWhen (true)] out byte[]? token) + { + if (ChannelBindingContext == null) { + token = null; + return false; + } + + return ChannelBindingContext.TryGetChannelBindingToken (kind, out token); + } + + static byte[]? Base64Decode (string? token, out int length) + { + byte[]? decoded = null; + + length = 0; + + if (token != null) { + try { + decoded = Convert.FromBase64String (token); + length = decoded.Length; + } catch (FormatException) { + } + } + + return decoded; + } + + static string Base64Encode (byte[]? challenge) + { + if (challenge == null || challenge.Length == 0) + return string.Empty; + + return Convert.ToBase64String (challenge); + } + + /// + /// Parse the server's challenge token and return the next challenge response. /// /// /// Parses the server's challenge token and returns the next challenge response. @@ -159,57 +355,103 @@ public Uri Uri { /// The server's challenge token. /// The index into the token specifying where the server's challenge begins. /// The length of the server's challenge. - /// - /// The SASL mechanism is already authenticated. - /// + /// The cancellation token. /// - /// THe SASL mechanism does not support SASL-IR. + /// The SASL mechanism does not support SASL-IR. + /// + /// + /// The operation was canceled via the cancellation token. /// /// /// An error has occurred while parsing the server's challenge token. /// - protected abstract byte[] Challenge (byte[] token, int startIndex, int length); + protected abstract byte[]? Challenge (byte[]? token, int startIndex, int length, CancellationToken cancellationToken); /// - /// Decodes the base64-encoded server challenge and returns the next challenge response encoded in base64. + /// Decode the base64-encoded server challenge and return the next challenge response encoded in base64. /// /// /// Decodes the base64-encoded server challenge and returns the next challenge response encoded in base64. /// /// The next base64-encoded challenge response. /// The server's base64-encoded challenge token. - /// - /// The SASL mechanism is already authenticated. + /// The cancellation token. + /// + /// The SASL mechanism does not support SASL-IR. + /// + /// + /// The operation was canceled via the cancellation token. /// + /// + /// An error has occurred while parsing the server's challenge token. + /// + public string Challenge (string? token, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested (); + + byte[]? decoded = Base64Decode (token?.Trim (), out int length); + + var challenge = Challenge (decoded, 0, length, cancellationToken); + + return Base64Encode (challenge); + } + + /// + /// Asynchronously parse the server's challenge token and return the next challenge response. + /// + /// + /// Asynchronously parses the server's challenge token and returns the next challenge response. + /// + /// The next challenge response. + /// The server's challenge token. + /// The index into the token specifying where the server's challenge begins. + /// The length of the server's challenge. + /// The cancellation token. /// - /// THe SASL mechanism does not support SASL-IR. + /// The SASL mechanism does not support SASL-IR. + /// + /// + /// The operation was canceled via the cancellation token. /// /// /// An error has occurred while parsing the server's challenge token. /// - public string Challenge (string token) + protected virtual Task ChallengeAsync (byte[]? token, int startIndex, int length, CancellationToken cancellationToken) { - byte[] decoded = null; - int length = 0; + return Task.FromResult (Challenge (token, startIndex, length, cancellationToken)); + } - if (token != null) { - try { - decoded = Convert.FromBase64String (token.Trim ()); - length = decoded.Length; - } catch (FormatException) { - } - } + /// + /// Asynchronously decode the base64-encoded server challenge and return the next challenge response encoded in base64. + /// + /// + /// Asynchronously decodes the base64-encoded server challenge and returns the next challenge response encoded in base64. + /// + /// The next base64-encoded challenge response. + /// The server's base64-encoded challenge token. + /// The cancellation token. + /// + /// The SASL mechanism does not support SASL-IR. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An error has occurred while parsing the server's challenge token. + /// + public async Task ChallengeAsync (string? token, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested (); - var challenge = Challenge (decoded, 0, length); + byte[]? decoded = Base64Decode (token?.Trim (), out int length); - if (challenge == null) - return null; + var challenge = await ChallengeAsync (decoded, 0, length, cancellationToken).ConfigureAwait (false); - return Convert.ToBase64String (challenge); + return Base64Encode (challenge); } /// - /// Resets the state of the SASL mechanism. + /// Reset the state of the SASL mechanism. /// /// /// Resets the state of the SASL mechanism. @@ -220,16 +462,16 @@ public virtual void Reset () } /// - /// Determines if the specified SASL mechanism is supported by MailKit. + /// Determine if the specified SASL mechanism is supported by MailKit. /// /// /// Use this method to make sure that a SASL mechanism is supported before calling - /// . + /// . /// - /// true if the specified SASL mechanism is supported; otherwise, false. + /// if the specified SASL mechanism is supported; otherwise, . /// The name of the SASL mechanism. /// - /// is null. + /// is . /// public static bool IsSupported (string mechanism) { @@ -237,47 +479,52 @@ public static bool IsSupported (string mechanism) throw new ArgumentNullException (nameof (mechanism)); switch (mechanism) { - case "SCRAM-SHA-256": return true; - case "SCRAM-SHA-1": return true; - case "DIGEST-MD5": return true; - case "CRAM-MD5": return true; - case "XOAUTH2": return true; - case "PLAIN": return true; - case "LOGIN": return true; - case "NTLM": return true; - default: return false; + case "SCRAM-SHA-512-PLUS": return true; + case "SCRAM-SHA-512": return true; + case "SCRAM-SHA-256-PLUS": return true; + case "SCRAM-SHA-256": return true; + case "SCRAM-SHA-1-PLUS": return true; + case "SCRAM-SHA-1": return true; + case "DIGEST-MD5": return Md5Supported; + case "CRAM-MD5": return Md5Supported; + case "OAUTHBEARER": return true; + case "XOAUTH2": return true; + case "PLAIN": return true; + case "LOGIN": return true; + case "NTLM": return true; + case "ANONYMOUS": return true; +#if NET7_0_OR_GREATER + case "GSSAPI": return GssapiSupported.Value; +#endif + default: return false; } } /// - /// Create an instance of the specified SASL mechanism using the uri and credentials. + /// Create an instance of the specified SASL mechanism using the supplied credentials. /// /// /// If unsure that a particular SASL mechanism is supported, you should first call /// . /// - /// An instance of the requested SASL mechanism if supported; otherwise null. + /// An instance of the requested SASL mechanism if supported; otherwise . /// The name of the SASL mechanism. - /// The URI of the service to authenticate against. /// The text encoding to use for the credentials. /// The user's credentials. /// - /// is null. - /// -or- - /// is null. + /// is . /// -or- - /// is null. + /// is . /// -or- - /// is null. + /// is . /// - public static SaslMechanism Create (string mechanism, Uri uri, Encoding encoding, ICredentials credentials) + public static SaslMechanism? Create (string mechanism, Encoding encoding, NetworkCredential credentials) { + // FIXME: This API should throw NotSupportedException rather than returning null if the mechanism is not supported. + if (mechanism == null) throw new ArgumentNullException (nameof (mechanism)); - if (uri == null) - throw new ArgumentNullException (nameof (uri)); - if (encoding == null) throw new ArgumentNullException (nameof (encoding)); @@ -285,50 +532,57 @@ public static SaslMechanism Create (string mechanism, Uri uri, Encoding encoding throw new ArgumentNullException (nameof (credentials)); switch (mechanism) { - //case "KERBEROS_V4": return null; - case "SCRAM-SHA-256": return new SaslMechanismScramSha256 (uri, credentials); - case "SCRAM-SHA-1": return new SaslMechanismScramSha1 (uri, credentials); - case "DIGEST-MD5": return new SaslMechanismDigestMd5 (uri, credentials); - case "CRAM-MD5": return new SaslMechanismCramMd5 (uri, credentials); - //case "GSSAPI": return null; - case "XOAUTH2": return new SaslMechanismOAuth2 (uri, credentials); - case "PLAIN": return new SaslMechanismPlain (uri, encoding, credentials); - case "LOGIN": return new SaslMechanismLogin (uri, encoding, credentials); - case "NTLM": return new SaslMechanismNtlm (uri, credentials); - default: return null; + //case "KERBEROS_V4": return null; + case "SCRAM-SHA-512-PLUS": return new SaslMechanismScramSha512Plus (credentials); + case "SCRAM-SHA-512": return new SaslMechanismScramSha512 (credentials); + case "SCRAM-SHA-256-PLUS": return new SaslMechanismScramSha256Plus (credentials); + case "SCRAM-SHA-256": return new SaslMechanismScramSha256 (credentials); + case "SCRAM-SHA-1-PLUS": return new SaslMechanismScramSha1Plus (credentials); + case "SCRAM-SHA-1": return new SaslMechanismScramSha1 (credentials); + case "DIGEST-MD5": return Md5Supported ? new SaslMechanismDigestMd5 (credentials) : null; + case "CRAM-MD5": return Md5Supported ? new SaslMechanismCramMd5 (credentials) : null; + case "OAUTHBEARER": return new SaslMechanismOAuthBearer (credentials); + case "XOAUTH2": return new SaslMechanismOAuth2 (credentials); + case "PLAIN": return new SaslMechanismPlain (encoding, credentials); + case "LOGIN": return new SaslMechanismLogin (encoding, credentials); +#if NET7_0_OR_GREATER + case "GSSAPI": return GssapiSupported.Value ? new SaslMechanismGssapi (credentials) : null; + case "NTLM": return NativeNtlmSupported.Value ? new SaslMechanismNtlmNative (credentials) : new SaslMechanismNtlm (credentials); +#else + case "NTLM": return new SaslMechanismNtlm (credentials); +#endif + case "ANONYMOUS": return new SaslMechanismAnonymous (encoding, credentials); + default: return null; } } /// - /// Create an instance of the specified SASL mechanism using the uri and credentials. + /// Create an instance of the specified SASL mechanism using the supplied credentials. /// /// /// If unsure that a particular SASL mechanism is supported, you should first call /// . /// - /// An instance of the requested SASL mechanism if supported; otherwise null. + /// An instance of the requested SASL mechanism if supported; otherwise . /// The name of the SASL mechanism. - /// The URI of the service to authenticate against. /// The user's credentials. /// - /// is null. - /// -or- - /// is null. + /// is . /// -or- - /// is null. + /// is . /// - public static SaslMechanism Create (string mechanism, Uri uri, ICredentials credentials) + public static SaslMechanism? Create (string mechanism, NetworkCredential credentials) { - return Create (mechanism, uri, Encoding.UTF8, credentials); + return Create (mechanism, Encoding.UTF8, credentials); } /// - /// Determines if the character is a non-ASCII space. + /// Determine if the character is a non-ASCII space. /// /// /// This list was obtained from http://tools.ietf.org/html/rfc3454#appendix-C.1.2 /// - /// true if the character is a non-ASCII space; otherwise, false. + /// if the character is a non-ASCII space; otherwise, . /// The character. static bool IsNonAsciiSpace (char c) { @@ -357,12 +611,12 @@ static bool IsNonAsciiSpace (char c) } /// - /// Determines if the character is commonly mapped to nothing. + /// Determine if the character is commonly mapped to nothing. /// /// /// This list was obtained from http://tools.ietf.org/html/rfc3454#appendix-B.1 /// - /// true if the character is commonly mapped to nothing; otherwise, false. + /// if the character is commonly mapped to nothing; otherwise, . /// The character. static bool IsCommonlyMappedToNothing (char c) { @@ -383,12 +637,12 @@ static bool IsCommonlyMappedToNothing (char c) } /// - /// Determines if the character is prohibited. + /// Determine if the character is prohibited. /// /// /// This list was obtained from http://tools.ietf.org/html/rfc3454#appendix-C.3 /// - /// true if the character is prohibited; otherwise, false. + /// if the character is prohibited; otherwise, . /// The string. /// The character index. static bool IsProhibited (string s, int index) @@ -454,7 +708,7 @@ static bool IsProhibited (string s, int index) } /// - /// Prepares the user name or password string. + /// Prepare the user name or password string. /// /// /// Prepares a user name or password string according to the rules of rfc4013. @@ -462,7 +716,7 @@ static bool IsProhibited (string s, int index) /// The prepared string. /// The string to prepare. /// - /// is null. + /// is . /// /// /// contains prohibited characters. @@ -478,7 +732,7 @@ public static string SaslPrep (string s) var builder = new StringBuilder (s.Length); for (int i = 0; i < s.Length; i++) { if (IsNonAsciiSpace (s[i])) { - // non-ASII space characters [StringPrep, C.1.2] that can be + // non-ASCII space characters [StringPrep, C.1.2] that can be // mapped to SPACE (U+0020). builder.Append (' '); } else if (IsCommonlyMappedToNothing (s[i])) { @@ -493,11 +747,17 @@ public static string SaslPrep (string s) } } -#if !NETFX_CORE && !NETSTANDARD return builder.ToString ().Normalize (NormalizationForm.FormKC); -#else - return builder.ToString (); -#endif + } + + internal static string GenerateEntropy (int n) + { + var entropy = new byte[n]; + + using (var rng = RandomNumberGenerator.Create ()) + rng.GetBytes (entropy); + + return Convert.ToBase64String (entropy); } } } diff --git a/MailKit/Security/SaslMechanismAnonymous.cs b/MailKit/Security/SaslMechanismAnonymous.cs new file mode 100644 index 0000000000..d8831bd6bb --- /dev/null +++ b/MailKit/Security/SaslMechanismAnonymous.cs @@ -0,0 +1,169 @@ +// +// SaslMechanismAnonymous.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Net; +using System.Text; +using System.Threading; + +namespace MailKit.Security { + /// + /// The ANONYMOUS SASL mechanism. + /// + /// + /// The ANONYMOUS SASL mechanism provides a way to authenticate with servers + /// that allow anonymous access. + /// + public class SaslMechanismAnonymous : SaslMechanism + { + readonly Encoding encoding; + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new ANONYMOUS SASL context. + /// + /// The encoding to use for the user's credentials. + /// The user's credentials. + /// + /// is . + /// -or- + /// is . + /// + public SaslMechanismAnonymous (Encoding encoding, NetworkCredential credentials) : base (credentials) + { + if (encoding == null) + throw new ArgumentNullException (nameof (encoding)); + + this.encoding = encoding; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new ANONYMOUS SASL context. + /// + /// The encoding to use for the user's credentials. + /// The user name. + /// + /// is . + /// -or- + /// is . + /// + public SaslMechanismAnonymous (Encoding encoding, string userName) : base (userName, string.Empty) + { + if (encoding == null) + throw new ArgumentNullException (nameof (encoding)); + + this.encoding = encoding; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new ANONYMOUS SASL context. + /// + /// The user's credentials. + /// + /// is . + /// + public SaslMechanismAnonymous (NetworkCredential credentials) : this (Encoding.UTF8, credentials) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new ANONYMOUS SASL context. + /// + /// The user name. + /// + /// is . + /// + public SaslMechanismAnonymous (string userName) : this (Encoding.UTF8, userName) + { + } + + /// + /// Get the name of the SASL mechanism. + /// + /// + /// Gets the name of the SASL mechanism. + /// + /// The name of the SASL mechanism. + public override string MechanismName { + get { return "ANONYMOUS"; } + } + + /// + /// Get whether or not the mechanism supports an initial response (SASL-IR). + /// + /// + /// Gets whether or not the mechanism supports an initial response (SASL-IR). + /// SASL mechanisms that support sending an initial client response to the server + /// should return . + /// + /// if the mechanism supports an initial response; otherwise, . + public override bool SupportsInitialResponse { + get { return true; } + } + + /// + /// Parse the server's challenge token and return the next challenge response. + /// + /// + /// Parses the server's challenge token and returns the next challenge response. + /// + /// The next challenge response. + /// The server's challenge token. + /// The index into the token specifying where the server's challenge begins. + /// The length of the server's challenge. + /// The cancellation token. + /// + /// The SASL mechanism does not support SASL-IR. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An error has occurred while parsing the server's challenge token. + /// + protected override byte[]? Challenge (byte[]? token, int startIndex, int length, CancellationToken cancellationToken) + { + if (IsAuthenticated) + return null; + + var buffer = encoding.GetBytes (Credentials.UserName); + IsAuthenticated = true; + + return buffer; + } + } +} diff --git a/MailKit/Security/SaslMechanismCramMd5.cs b/MailKit/Security/SaslMechanismCramMd5.cs index 473d10c840..288e6f5e97 100644 --- a/MailKit/Security/SaslMechanismCramMd5.cs +++ b/MailKit/Security/SaslMechanismCramMd5.cs @@ -1,9 +1,9 @@ -// +// // SaslMechanismCramMd5.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -27,12 +27,8 @@ using System; using System.Net; using System.Text; - -#if NETFX_CORE || NETSTANDARD -using MD5 = MimeKit.Cryptography.MD5; -#else +using System.Threading; using System.Security.Cryptography; -#endif namespace MailKit.Security { /// @@ -54,30 +50,44 @@ public class SaslMechanismCramMd5 : SaslMechanism /// /// Creates a new CRAM-MD5 SASL context. /// - /// The URI of the service. /// The user's credentials. /// - /// is null. + /// is . + /// + public SaslMechanismCramMd5 (NetworkCredential credentials) : base (credentials) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new CRAM-MD5 SASL context. + /// + /// The user name. + /// The password. + /// + /// is . /// -or- - /// is null. + /// is . /// - public SaslMechanismCramMd5 (Uri uri, ICredentials credentials) : base (uri, credentials) + public SaslMechanismCramMd5 (string userName, string password) : base (userName, password) { } /// - /// Gets the name of the mechanism. + /// Get the name of the SASL mechanism. /// /// - /// Gets the name of the mechanism. + /// Gets the name of the SASL mechanism. /// - /// The name of the mechanism. + /// The name of the SASL mechanism. public override string MechanismName { get { return "CRAM-MD5"; } } /// - /// Parses the server's challenge token and returns the next challenge response. + /// Parse the server's challenge token and return the next challenge response. /// /// /// Parses the server's challenge token and returns the next challenge response. @@ -86,43 +96,42 @@ public override string MechanismName { /// The server's challenge token. /// The index into the token specifying where the server's challenge begins. /// The length of the server's challenge. - /// - /// The SASL mechanism is already authenticated. - /// + /// The cancellation token. /// /// The SASL mechanism does not support SASL-IR. /// + /// + /// The operation was canceled via the cancellation token. + /// /// /// An error has occurred while parsing the server's challenge token. /// - protected override byte[] Challenge (byte[] token, int startIndex, int length) + protected override byte[]? Challenge (byte[]? token, int startIndex, int length, CancellationToken cancellationToken) { - if (IsAuthenticated) - throw new InvalidOperationException (); - if (token == null) throw new NotSupportedException ("CRAM-MD5 does not support SASL-IR."); - var cred = Credentials.GetCredential (Uri, MechanismName); - var userName = Encoding.UTF8.GetBytes (cred.UserName); - var password = Encoding.UTF8.GetBytes (cred.Password); + if (IsAuthenticated) + return null; + + var userName = Encoding.UTF8.GetBytes (Credentials.UserName); + var password = Encoding.UTF8.GetBytes (Credentials.Password); var ipad = new byte[64]; var opad = new byte[64]; - byte[] digest; + byte[] digest, passwd; if (password.Length > 64) { - byte[] checksum; - using (var md5 = MD5.Create ()) - checksum = md5.ComputeHash (password); - - Array.Copy (checksum, ipad, checksum.Length); - Array.Copy (checksum, opad, checksum.Length); + passwd = md5.ComputeHash (password); } else { - Array.Copy (password, ipad, password.Length); - Array.Copy (password, opad, password.Length); + passwd = password; } + Array.Copy (passwd, ipad, passwd.Length); + Array.Copy (passwd, opad, passwd.Length); + + Array.Clear (password, 0, password.Length); + for (int i = 0; i < 64; i++) { ipad[i] ^= 0x36; opad[i] ^= 0x5c; @@ -131,13 +140,13 @@ protected override byte[] Challenge (byte[] token, int startIndex, int length) using (var md5 = MD5.Create ()) { md5.TransformBlock (ipad, 0, ipad.Length, null, 0); md5.TransformFinalBlock (token, startIndex, length); - digest = md5.Hash; + digest = md5.Hash!; } using (var md5 = MD5.Create ()) { md5.TransformBlock (opad, 0, opad.Length, null, 0); md5.TransformFinalBlock (digest, 0, digest.Length); - digest = md5.Hash; + digest = md5.Hash!; } var buffer = new byte[userName.Length + 1 + (digest.Length * 2)]; diff --git a/MailKit/Security/SaslMechanismDigestMd5.cs b/MailKit/Security/SaslMechanismDigestMd5.cs index 0f823b941c..ec3197699d 100644 --- a/MailKit/Security/SaslMechanismDigestMd5.cs +++ b/MailKit/Security/SaslMechanismDigestMd5.cs @@ -1,9 +1,9 @@ -// +// // SaslMechanismDigestMd5.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -27,17 +27,13 @@ using System; using System.Net; using System.Text; +using System.Threading; +using System.Globalization; using System.Collections.Generic; - -#if NETFX_CORE -using Encoding = Portable.Text.Encoding; -using MD5 = MimeKit.Cryptography.MD5; -#elif NETSTANDARD -using System.Security.Cryptography; -using MD5 = MimeKit.Cryptography.MD5; -#else using System.Security.Cryptography; -#endif +using System.Diagnostics.CodeAnalysis; + +using MimeKit.Utils; namespace MailKit.Security { /// @@ -55,10 +51,11 @@ enum LoginState { Final } - DigestChallenge challenge; - DigestResponse response; + DigestChallenge? challenge; + DigestResponse? response; + internal string? cnonce; + Encoding? encoding; LoginState state; - string cnonce; /// /// Initializes a new instance of the class. @@ -66,17 +63,12 @@ enum LoginState { /// /// Creates a new DIGEST-MD5 SASL context. /// - /// The URI of the service. /// The user's credentials. - /// Random characters to act as the cnonce token. /// - /// is null. - /// -or- - /// is null. + /// is . /// - internal SaslMechanismDigestMd5 (Uri uri, ICredentials credentials, string entropy) : base (uri, credentials) + public SaslMechanismDigestMd5 (NetworkCredential credentials) : base (credentials) { - cnonce = entropy; } /// @@ -85,30 +77,42 @@ internal SaslMechanismDigestMd5 (Uri uri, ICredentials credentials, string entro /// /// Creates a new DIGEST-MD5 SASL context. /// - /// The URI of the service. - /// The user's credentials. + /// The user name. + /// The password. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// - public SaslMechanismDigestMd5 (Uri uri, ICredentials credentials) : base (uri, credentials) + public SaslMechanismDigestMd5 (string userName, string password) : base (userName, password) { } /// - /// Gets the name of the mechanism. + /// Get or set the authorization identifier. + /// + /// + /// The authorization identifier is the desired user account that the server should use + /// for all accesses. This is separate from the user name used for authentication. + /// + /// The authorization identifier. + public string? AuthorizationId { + get; set; + } + + /// + /// Get the name of the SASL mechanism. /// /// - /// Gets the name of the mechanism. + /// Gets the name of the SASL mechanism. /// - /// The name of the mechanism. + /// The name of the SASL mechanism. public override string MechanismName { get { return "DIGEST-MD5"; } } /// - /// Parses the server's challenge token and returns the next challenge response. + /// Parse the server's challenge token and return the next challenge response. /// /// /// Parses the server's challenge token and returns the next challenge response. @@ -117,68 +121,66 @@ public override string MechanismName { /// The server's challenge token. /// The index into the token specifying where the server's challenge begins. /// The length of the server's challenge. - /// - /// The SASL mechanism is already authenticated. - /// + /// The cancellation token. /// - /// THe SASL mechanism does not support SASL-IR. + /// The SASL mechanism does not support SASL-IR. + /// + /// + /// The operation was canceled via the cancellation token. /// /// /// An error has occurred while parsing the server's challenge token. /// - protected override byte[] Challenge (byte[] token, int startIndex, int length) + protected override byte[]? Challenge (byte[]? token, int startIndex, int length, CancellationToken cancellationToken) { if (IsAuthenticated) - throw new InvalidOperationException (); + return null; - if (token == null) - throw new NotSupportedException ("DIGEST-MD5 does not support SASL-IR."); - - var cred = Credentials.GetCredential (Uri, MechanismName); + if (Uri is null) + throw new InvalidOperationException (); switch (state) { case LoginState.Auth: + if (token == null) + throw new NotSupportedException ("DIGEST-MD5 does not support SASL-IR."); + if (token.Length > 2048) throw new SaslException (MechanismName, SaslErrorCode.ChallengeTooLong, "Server challenge too long."); challenge = DigestChallenge.Parse (Encoding.UTF8.GetString (token, startIndex, length)); + encoding = challenge.Charset != null ? Encoding.UTF8 : TextEncodings.Latin1; + cnonce ??= GenerateEntropy (15); - if (string.IsNullOrEmpty (cnonce)) { - var entropy = new byte[15]; - - using (var rng = RandomNumberGenerator.Create ()) - rng.GetBytes (entropy); - - cnonce = Convert.ToBase64String (entropy); - } - - response = new DigestResponse (challenge, Uri.Scheme, Uri.DnsSafeHost, cred.UserName, cred.Password, cnonce); + response = new DigestResponse (challenge, encoding, Uri.Scheme, Uri.DnsSafeHost, AuthorizationId, Credentials.UserName, Credentials.Password, cnonce); state = LoginState.Final; - return response.Encode (); + + return response.Encode (encoding); case LoginState.Final: - if (token.Length == 0) + if (token == null || token.Length == 0) throw new SaslException (MechanismName, SaslErrorCode.MissingChallenge, "Server response did not contain any authentication data."); - var text = Encoding.UTF8.GetString (token, startIndex, length); - string key, value; - int index = 0; + var text = encoding!.GetString (token, startIndex, length); + string? key, value; - if (!DigestChallenge.TryParseKeyValuePair (text, ref index, out key, out value)) + if (!DigestChallenge.TryParseKeyValuePair (text, out key, out value)) throw new SaslException (MechanismName, SaslErrorCode.IncompleteChallenge, "Server response contained incomplete authentication data."); - var expected = response.ComputeHash (cred.Password, false); + if (!key.Equals ("rspauth", StringComparison.OrdinalIgnoreCase)) + throw new SaslException (MechanismName, SaslErrorCode.InvalidChallenge, "Server response contained invalid data."); + + var expected = response!.ComputeHash (encoding, Credentials.Password, false); if (value != expected) throw new SaslException (MechanismName, SaslErrorCode.IncorrectHash, "Server response did not contain the expected hash."); IsAuthenticated = true; - return new byte[0]; - default: - throw new IndexOutOfRangeException ("state"); + break; } + + return null; } /// - /// Resets the state of the SASL mechanism. + /// Reset the state of the SASL mechanism. /// /// /// Resets the state of the SASL mechanism. @@ -195,19 +197,25 @@ public override void Reset () class DigestChallenge { - public string[] Realms { get; private set; } - public string Nonce { get; private set; } + public string[]? Realms { get; private set; } + public string? Nonce { get; private set; } public HashSet Qop { get; private set; } - public bool Stale { get; private set; } - public int MaxBuf { get; private set; } - public string Charset { get; private set; } - public string Algorithm { get; private set; } + public bool? Stale { get; private set; } + public int? MaxBuf { get; private set; } + public string? Charset { get; private set; } + public string? Algorithm { get; private set; } public HashSet Ciphers { get; private set; } - DigestChallenge () + DigestChallenge (string nonce, string? algorithm, string? charset, string[]? ciphers, string[]? realms, string[]? qop, bool? stale, int? maxbuf) { - Ciphers = new HashSet (); - Qop = new HashSet (); + Ciphers = ciphers != null ? new HashSet (ciphers, StringComparer.Ordinal) : new HashSet (StringComparer.Ordinal); + Qop = qop != null ? new HashSet (qop, StringComparer.Ordinal) : new HashSet (StringComparer.Ordinal); + Algorithm = algorithm; + Charset = charset; + MaxBuf = maxbuf; + Realms = realms; + Nonce = nonce; + Stale = stale; } static bool SkipWhiteSpace (string text, ref int index) @@ -220,24 +228,17 @@ static bool SkipWhiteSpace (string text, ref int index) return index > startIndex; } - static bool TryParseKey (string text, ref int index, out string key) + static string GetKey (string text, ref int index) { int startIndex = index; - key = null; - while (index < text.Length && !char.IsWhiteSpace (text[index]) && text[index] != '=' && text[index] != ',') index++; - if (index == startIndex) - return false; - - key = text.Substring (startIndex, index - startIndex); - - return true; + return text.Substring (startIndex, index - startIndex); } - static bool TryParseQuoted (string text, ref int index, out string value) + static bool TryParseQuoted (string text, ref int index, [NotNullWhen (true)] out string? value) { var builder = new StringBuilder (); bool escaped = false; @@ -275,35 +276,26 @@ static bool TryParseQuoted (string text, ref int index, out string value) return true; } - static bool TryParseValue (string text, ref int index, out string value) + static bool TryParseValue (string text, ref int index, [NotNullWhen (true)] out string? value) { if (text[index] == '"') return TryParseQuoted (text, ref index, out value); int startIndex = index; - value = null; - while (index < text.Length && !char.IsWhiteSpace (text[index]) && text[index] != ',') index++; - if (index == startIndex) - return false; - value = text.Substring (startIndex, index - startIndex); return true; } - public static bool TryParseKeyValuePair (string text, ref int index, out string key, out string value) + static bool TryParseKeyValuePair (string text, ref int index, [NotNullWhen (true)] out string? key, [NotNullWhen (true)] out string? value) { value = null; - key = null; - SkipWhiteSpace (text, ref index); - - if (!TryParseKey (text, ref index, out key)) - return false; + key = GetKey (text, ref index); SkipWhiteSpace (text, ref index); if (index >= text.Length || text[index] != '=') @@ -313,56 +305,98 @@ public static bool TryParseKeyValuePair (string text, ref int index, out string index++; SkipWhiteSpace (text, ref index); + if (index >= text.Length) + return false; return TryParseValue (text, ref index, out value); } + public static bool TryParseKeyValuePair (string text, [NotNullWhen (true)] out string? key, [NotNullWhen (true)] out string? value) + { + int index = 0; + + value = null; + key = null; + + SkipWhiteSpace (text, ref index); + if (index >= text.Length || !TryParseKeyValuePair (text, ref index, out key, out value)) + return false; + + return true; + } + + static readonly char[] Comma = new char[] { ',' }; + public static DigestChallenge Parse (string token) { - var challenge = new DigestChallenge (); + string[]? realms = null, qop = null, ciphers = null; + string? algorithm = null; + string? charset = null; + string? nonce = null; + bool? stale = null; + int maxbuf = -1; int index = 0; - while (index < token.Length) { - string key, value; + SkipWhiteSpace (token, ref index); - if (!TryParseKeyValuePair (token, ref index, out key, out value)) + while (index < token.Length) { + if (!TryParseKeyValuePair (token, ref index, out var key, out var value)) throw new SaslException ("DIGEST-MD5", SaslErrorCode.InvalidChallenge, string.Format ("Invalid SASL challenge from the server: {0}", token)); switch (key.ToLowerInvariant ()) { case "realm": - challenge.Realms = value.Split (new [] { ',' }, StringSplitOptions.RemoveEmptyEntries); + if (realms != null) + throw new SaslException ("DIGEST-MD5", SaslErrorCode.InvalidChallenge, string.Format ("Invalid SASL challenge from the server: {0}", token)); + realms = value.Split (Comma, StringSplitOptions.RemoveEmptyEntries); break; case "nonce": - challenge.Nonce = value; + if (nonce != null) + throw new SaslException ("DIGEST-MD5", SaslErrorCode.InvalidChallenge, string.Format ("Invalid SASL challenge from the server: {0}", token)); + nonce = value; break; case "qop": - foreach (var qop in value.Split (new [] { ',' }, StringSplitOptions.RemoveEmptyEntries)) - challenge.Qop.Add (qop.Trim ()); + if (qop != null) + throw new SaslException ("DIGEST-MD5", SaslErrorCode.InvalidChallenge, string.Format ("Invalid SASL challenge from the server: {0}", token)); + qop = value.Split (Comma, StringSplitOptions.RemoveEmptyEntries); break; case "stale": - challenge.Stale = value.ToLowerInvariant () == "true"; + if (stale.HasValue) + throw new SaslException ("DIGEST-MD5", SaslErrorCode.InvalidChallenge, string.Format ("Invalid SASL challenge from the server: {0}", token)); + stale = value.Equals ("true", StringComparison.OrdinalIgnoreCase); break; case "maxbuf": - challenge.MaxBuf = int.Parse (value); + if (maxbuf != -1 || !int.TryParse (value, NumberStyles.None, CultureInfo.InvariantCulture, out maxbuf)) + throw new SaslException ("DIGEST-MD5", SaslErrorCode.InvalidChallenge, string.Format ("Invalid SASL challenge from the server: {0}", token)); break; case "charset": - challenge.Charset = value; + if (charset != null || !value.Equals ("utf-8", StringComparison.OrdinalIgnoreCase)) + throw new SaslException ("DIGEST-MD5", SaslErrorCode.InvalidChallenge, string.Format ("Invalid SASL challenge from the server: {0}", token)); + charset = "utf-8"; break; case "algorithm": - challenge.Algorithm = value; + if (algorithm != null) + throw new SaslException ("DIGEST-MD5", SaslErrorCode.InvalidChallenge, string.Format ("Invalid SASL challenge from the server: {0}", token)); + algorithm = value; break; case "cipher": - foreach (var cipher in value.Split (new [] { ',' }, StringSplitOptions.RemoveEmptyEntries)) - challenge.Ciphers.Add (cipher.Trim ()); + if (ciphers != null) + throw new SaslException ("DIGEST-MD5", SaslErrorCode.InvalidChallenge, string.Format ("Invalid SASL challenge from the server: {0}", token)); + ciphers = value.Split (Comma, StringSplitOptions.RemoveEmptyEntries); break; } SkipWhiteSpace (token, ref index); - if (index < token.Length && token[index] == ',') + if (index < token.Length && token[index] == ',') { index++; + + SkipWhiteSpace (token, ref index); + } } - return challenge; + if (nonce == null) + throw new SaslException ("DIGEST-MD5", SaslErrorCode.InvalidChallenge, string.Format ("Invalid SASL challenge from the server: {0}", token)); + + return new DigestChallenge (nonce, algorithm, charset, ciphers, realms, qop, stale, maxbuf != -1 ? maxbuf : null); } } @@ -370,19 +404,19 @@ class DigestResponse { public string UserName { get; private set; } public string Realm { get; private set; } - public string Nonce { get; private set; } + public string? Nonce { get; private set; } public string CNonce { get; private set; } public int Nc { get; private set; } public string Qop { get; private set; } public string DigestUri { get; private set; } public string Response { get; private set; } - public int MaxBuf { get; private set; } - public string Charset { get; private set; } - public string Algorithm { get; private set; } - public string Cipher { get; private set; } - public string AuthZid { get; private set; } + public int? MaxBuf { get; private set; } + public string? Charset { get; private set; } + public string? Algorithm { get; private set; } + public string? Cipher { get; private set; } + public string? AuthZid { get; private set; } - public DigestResponse (DigestChallenge challenge, string protocol, string hostName, string userName, string password, string cnonce) + public DigestResponse (DigestChallenge challenge, Encoding encoding, string protocol, string hostName, string? authzid, string userName, string password, string cnonce) { UserName = userName; @@ -399,15 +433,13 @@ public DigestResponse (DigestChallenge challenge, string protocol, string hostNa Qop = "auth"; DigestUri = string.Format ("{0}/{1}", protocol, hostName); - - if (!string.IsNullOrEmpty (challenge.Charset)) - Charset = challenge.Charset; - Algorithm = challenge.Algorithm; - AuthZid = null; + Charset = challenge.Charset; + MaxBuf = challenge.MaxBuf; + AuthZid = authzid; Cipher = null; - Response = ComputeHash (password, true); + Response = ComputeHash (encoding, password, true); } static string HexEncode (byte[] digest) @@ -420,14 +452,14 @@ static string HexEncode (byte[] digest) return hex.ToString (); } - public string ComputeHash (string password, bool client) + public string ComputeHash (Encoding encoding, string password, bool client) { string text, a1, a2; byte[] buf, digest; // compute A1 text = string.Format ("{0}:{1}:{2}", UserName, Realm, password); - buf = Encoding.UTF8.GetBytes (text); + buf = encoding.GetBytes (text); using (var md5 = MD5.Create ()) digest = md5.ComputeHash (buf); @@ -436,9 +468,9 @@ public string ComputeHash (string password, bool client) text = string.Format (":{0}:{1}", Nonce, CNonce); if (!string.IsNullOrEmpty (AuthZid)) text += ":" + AuthZid; - buf = Encoding.ASCII.GetBytes (text); + buf = encoding.GetBytes (text); md5.TransformFinalBlock (buf, 0, buf.Length); - a1 = HexEncode (md5.Hash); + a1 = HexEncode (md5.Hash!); } // compute A2 @@ -448,46 +480,25 @@ public string ComputeHash (string password, bool client) if (Qop == "auth-int" || Qop == "auth-conf") text += ":00000000000000000000000000000000"; - buf = Encoding.ASCII.GetBytes (text); + buf = encoding.GetBytes (text); using (var md5 = MD5.Create ()) digest = md5.ComputeHash (buf); a2 = HexEncode (digest); // compute KD text = string.Format ("{0}:{1}:{2:x8}:{3}:{4}:{5}", a1, Nonce, Nc, CNonce, Qop, a2); - buf = Encoding.ASCII.GetBytes (text); + buf = encoding.GetBytes (text); using (var md5 = MD5.Create ()) digest = md5.ComputeHash (buf); return HexEncode (digest); } - static string Quote (string text) - { - var quoted = new StringBuilder (); - - quoted.Append ("\""); - for (int i = 0; i < text.Length; i++) { - if (text[i] == '\\' || text[i] == '"') - quoted.Append ('\\'); - quoted.Append (text[i]); - } - quoted.Append ("\""); - - return quoted.ToString (); - } - - public byte[] Encode () + public byte[] Encode (Encoding encoding) { - Encoding encoding; - - if (!string.IsNullOrEmpty (Charset)) - encoding = Encoding.GetEncoding (Charset); - else - encoding = Encoding.UTF8; - var builder = new StringBuilder (); - builder.AppendFormat ("username={0}", Quote (UserName)); + builder.Append ("username="); + MimeUtils.AppendQuoted (builder, UserName); builder.AppendFormat (",realm=\"{0}\"", Realm); builder.AppendFormat (",nonce=\"{0}\"", Nonce); builder.AppendFormat (",cnonce=\"{0}\"", CNonce); @@ -495,8 +506,8 @@ public byte[] Encode () builder.AppendFormat (",qop=\"{0}\"", Qop); builder.AppendFormat (",digest-uri=\"{0}\"", DigestUri); builder.AppendFormat (",response={0}", Response); - if (MaxBuf > 0) - builder.AppendFormat (",maxbuf={0}", MaxBuf); + if (MaxBuf.HasValue) + builder.AppendFormat (CultureInfo.InvariantCulture, ",maxbuf={0}", MaxBuf.Value); if (!string.IsNullOrEmpty (Charset)) builder.AppendFormat (",charset={0}", Charset); if (!string.IsNullOrEmpty (Algorithm)) diff --git a/MailKit/Security/SaslMechanismGssapi.cs b/MailKit/Security/SaslMechanismGssapi.cs new file mode 100644 index 0000000000..e1df6e87bd --- /dev/null +++ b/MailKit/Security/SaslMechanismGssapi.cs @@ -0,0 +1,160 @@ +// +// SaslMechanismGssapi.cs +// +// Authors: Roman Konecny +// Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +#if NET7_0_OR_GREATER + +using System; +using System.Net; +using System.Net.Security; + +namespace MailKit.Security { + /// + /// A SASL mechanism that uses the Kerberos/GSSAPI protocol. + /// + /// + /// Implements the GSSAPI for KERBEROS SASL mechanism. + /// + public class SaslMechanismGssapi : SaslMechanismNegotiateBase + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new GSSAPI SASL context using the default network credentials. + /// + public SaslMechanismGssapi () : this (CredentialCache.DefaultNetworkCredentials) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new GSSAPI SASL context. + /// + /// The user's credentials. + /// + /// is . + /// + public SaslMechanismGssapi (NetworkCredential credentials) : base (credentials) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new GSSAPI SASL context. + /// + /// The user name. + /// The password. + /// + /// is . + /// -or- + /// is . + /// + public SaslMechanismGssapi (string userName, string password) : base (userName, password) + { + } + + /// + /// Get the name of the authentication mechanism. + /// + /// + /// Gets the name of the authentication mechanism. + /// This value MUST be one of the following: "NTLM", "Kerberos" or "Negotiate". + /// + /// The name of the authentication mechanism. + protected override string AuthMechanism { + get { return "Kerberos"; } + } + + /// + /// Get the name of the SASL mechanism. + /// + /// + /// Gets the name of the SASL mechanism. + /// + /// The name of the SASL mechanism. + public override string MechanismName { + get { return "GSSAPI"; } + } + + /// + /// Get whether or not the SASL mechanism supports negotiating a security layer. + /// + /// + /// Gets whether or not the SASL mechanism supports negotiating a security layer. + /// + /// if the SASL mechanism supports negotiating a security layer; otherwise, . + protected override bool SupportsSecurityLayer { + get { return true; } + } + + /// + /// Get the required protection level. + /// + /// + /// Gets the required protection level. + /// + /// The required protection level. + protected override ProtectionLevel RequiredProtectionLevel { + get { + // Work-around for https://github.com/gssapi/gss-ntlmssp/issues/77 + // GSSAPI NTLM SSP does not support gss_wrap/gss_unwrap unless confidentiality + // is negotiated. + if (OperatingSystem.IsLinux ()) + return ProtectionLevel.EncryptAndSign; + + return ProtectionLevel.Sign; + } + } + + /// + /// Create the . + /// + /// + /// Creates the . + /// + /// The client options. + protected override NegotiateAuthenticationClientOptions CreateClientOptions () + { + var options = base.CreateClientOptions (); + + if (Uri is null) + throw new InvalidOperationException (); + + // Provide a default TargetName (the base implementation already sets the + // TargetName to the ServicePrincipalName if the value was provided). + options.TargetName ??= $"SMTPSVC/{Uri.Host}"; + + return options; + } + } +} + +#endif // NET7_0_OR_GREATER diff --git a/MailKit/Security/SaslMechanismLogin.cs b/MailKit/Security/SaslMechanismLogin.cs index 40075efaab..36af22afea 100644 --- a/MailKit/Security/SaslMechanismLogin.cs +++ b/MailKit/Security/SaslMechanismLogin.cs @@ -1,9 +1,9 @@ -// +// // SaslMechanismLogin.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -27,10 +27,7 @@ using System; using System.Net; using System.Text; - -#if NETFX_CORE -using Encoding = Portable.Text.Encoding; -#endif +using System.Threading; namespace MailKit.Security { /// @@ -49,7 +46,7 @@ enum LoginState { Password } - Encoding encoding; + readonly Encoding encoding; LoginState state; /// @@ -58,17 +55,38 @@ enum LoginState { /// /// Creates a new LOGIN SASL context. /// - /// The URI of the service. /// The encoding to use for the user's credentials. /// The user's credentials. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . + /// + public SaslMechanismLogin (Encoding encoding, NetworkCredential credentials) : base (credentials) + { + if (encoding == null) + throw new ArgumentNullException (nameof (encoding)); + + this.encoding = encoding; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new LOGIN SASL context. + /// + /// The encoding to use for the user's credentials. + /// The user name. + /// The password. + /// + /// is . + /// -or- + /// is . /// -or- - /// is null. + /// is . /// - public SaslMechanismLogin (Uri uri, Encoding encoding, ICredentials credentials) : base (uri, credentials) + public SaslMechanismLogin (Encoding encoding, string userName, string password) : base (userName, password) { if (encoding == null) throw new ArgumentNullException (nameof (encoding)); @@ -82,37 +100,51 @@ public SaslMechanismLogin (Uri uri, Encoding encoding, ICredentials credentials) /// /// Creates a new LOGIN SASL context. /// - /// The URI of the service. /// The user's credentials. /// - /// is null. + /// is . + /// + public SaslMechanismLogin (NetworkCredential credentials) : this (Encoding.UTF8, credentials) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new LOGIN SASL context. + /// + /// The user name. + /// The password. + /// + /// is . /// -or- - /// is null. + /// is . /// - public SaslMechanismLogin (Uri uri, ICredentials credentials) : base (uri, credentials) + public SaslMechanismLogin (string userName, string password) : this (Encoding.UTF8, userName, password) { - encoding = Encoding.UTF8; } /// - /// Gets the name of the mechanism. + /// Get the name of the SASL mechanism. /// /// - /// Gets the name of the mechanism. + /// Gets the name of the SASL mechanism. /// - /// The name of the mechanism. + /// The name of the SASL mechanism. public override string MechanismName { get { return "LOGIN"; } } /// - /// Gets whether or not the mechanism supports an initial response (SASL-IR). + /// Get whether or not the mechanism supports an initial response (SASL-IR). /// /// - /// SASL mechanisms that support sending an initial client response to the server - /// should return true. + /// Gets whether or not the mechanism supports an initial response (SASL-IR). + /// SASL mechanisms that support sending an initial client response to the server + /// should return . /// - /// true if the mechanism supports an initial response; otherwise, false. + /// if the mechanism supports an initial response; otherwise, . public override bool SupportsInitialResponse { get { return false; } } @@ -127,34 +159,35 @@ public override bool SupportsInitialResponse { /// The server's challenge token. /// The index into the token specifying where the server's challenge begins. /// The length of the server's challenge. - /// - /// The SASL mechanism is already authenticated. - /// + /// The cancellation token. /// /// The SASL mechanism does not support SASL-IR. /// + /// + /// The operation was canceled via the cancellation token. + /// /// /// An error has occurred while parsing the server's challenge token. /// - protected override byte[] Challenge (byte[] token, int startIndex, int length) + protected override byte[]? Challenge (byte[]? token, int startIndex, int length, CancellationToken cancellationToken) { - var cred = Credentials.GetCredential (Uri, MechanismName); - byte[] challenge; + if (IsAuthenticated) + return null; - if (token == null) - throw new NotSupportedException ("LOGIN does not support SASL-IR."); + byte[]? challenge = null; switch (state) { case LoginState.UserName: - challenge = encoding.GetBytes (cred.UserName); + if (token == null) + throw new NotSupportedException ("LOGIN does not support SASL-IR."); + + challenge = encoding.GetBytes (Credentials.UserName); state = LoginState.Password; break; case LoginState.Password: - challenge = encoding.GetBytes (cred.Password); + challenge = encoding.GetBytes (Credentials.Password); IsAuthenticated = true; break; - default: - throw new InvalidOperationException (); } return challenge; diff --git a/MailKit/Security/SaslMechanismNegotiateBase.cs b/MailKit/Security/SaslMechanismNegotiateBase.cs new file mode 100644 index 0000000000..d75b18eede --- /dev/null +++ b/MailKit/Security/SaslMechanismNegotiateBase.cs @@ -0,0 +1,410 @@ +// +// SaslMechanismNegotiateBase.cs +// +// Authors: Roman Konecny +// Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +#if NET7_0_OR_GREATER + +using System; +using System.Net; +using System.Buffers; +using System.Threading; +using System.Net.Security; +using System.Security.Authentication.ExtendedProtection; + +namespace MailKit.Security +{ + /// + /// The base class for .NET Core's NegotiateAuthentication-based SASL mechanisms. + /// + /// + /// The base class for .NET Core's -based SASL mechanisms. + /// + public abstract class SaslMechanismNegotiateBase : SaslMechanism + { + static ReadOnlySpan SaslNoSecurityLayerToken => new byte[] { 1, 0, 0, 0 }; + + NegotiateAuthentication? negotiate; + bool negotiatedChannelBinding; + bool requestedChannelBinding; + bool negotiatedSecurityLayer; + + static SaslException GetSaslException (string mechanismName, NegotiateAuthenticationStatusCode statusCode) + { + var errorCode = SaslErrorCode.InvalidChallenge; + string message; + + switch (statusCode) { + case NegotiateAuthenticationStatusCode.GenericFailure: message = "Operation resulted in failure but no specific error code was given."; break; + case NegotiateAuthenticationStatusCode.BadBinding: message = "Channel binding mismatch between client and server."; break; + case NegotiateAuthenticationStatusCode.Unsupported: message = "Unsupported authentication package was requested."; break; + case NegotiateAuthenticationStatusCode.MessageAltered: message = "Message was altered and failed an integrity check validation."; break; + case NegotiateAuthenticationStatusCode.ContextExpired: message = "Referenced authentication context has expired."; break; + case NegotiateAuthenticationStatusCode.CredentialsExpired: message = "Authentication credentials have expired."; break; + case NegotiateAuthenticationStatusCode.InvalidCredentials: message = "Consistency checks performed on the credential failed."; break; + case NegotiateAuthenticationStatusCode.InvalidToken: message = "Checks performed on the authentication token failed."; break; + case NegotiateAuthenticationStatusCode.UnknownCredentials: message = "The supplied credentials were not valid for context acceptance, or the credential handle did not reference any credentials."; break; + case NegotiateAuthenticationStatusCode.QopNotSupported: message = "Requested protection level is not supported."; break; + case NegotiateAuthenticationStatusCode.OutOfSequence: message = "Authentication token was identfied as duplicate, old, or out of expected sequence."; break; + case NegotiateAuthenticationStatusCode.SecurityQosFailed: message = "Validation of RequiredProtectionLevel against negotiated protection level failed."; break; + case NegotiateAuthenticationStatusCode.TargetUnknown: message = "Validation of the target name failed."; break; + case NegotiateAuthenticationStatusCode.ImpersonationValidationFailed: message = "Validation of the impersonation level failed."; break; + default: message = $"Failed with unknown status code {statusCode}."; break; + } + + return new SaslException (mechanismName, errorCode, $"{mechanismName} authentication error: {message}"); + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new -based SASL context. + /// + /// The user's credentials. + /// + /// is . + /// + protected SaslMechanismNegotiateBase (NetworkCredential credentials) : base (credentials) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new -based SASL context. + /// + /// The user name. + /// The password. + /// + /// is . + /// -or- + /// is . + /// + protected SaslMechanismNegotiateBase (string userName, string password) : base (userName, password) + { + } + + /// + /// Get whether or not the SASL mechanism supports an initial response (SASL-IR). + /// + /// + /// Gets whether or not the SASL mechanism supports an initial response (SASL-IR). + /// SASL mechanisms that support sending an initial client response to the server + /// should return . + /// + /// if the SASL mechanism supports an initial response; otherwise, . + public override bool SupportsInitialResponse { + get { return true; } + } + + /// + /// Get the name of the authentication mechanism. + /// + /// + /// Gets the name of the authentication mechanism. + /// This value MUST be one of the following: "NTLM", "Kerberos" or "Negotiate". + /// + /// The name of the authentication mechanism. + protected abstract string AuthMechanism { + get; + } + + /// + /// Get the required protection level. + /// + /// + /// Gets the required protection level. + /// + /// The required protection level. + protected virtual ProtectionLevel RequiredProtectionLevel { + get { return ProtectionLevel.None; } + } + + /// + /// Get whether or not the SASL mechanism supports channel binding. + /// + /// + /// Gets whether or not the SASL mechanism supports channel binding. + /// + /// if the SASL mechanism supports channel binding; otherwise, . + public override bool SupportsChannelBinding { + get { return true; } + } + + /// + /// Get whether or not channel-binding was negotiated by the SASL mechanism. + /// + /// + /// Gets whether or not channel-binding has been negotiated by the SASL mechanism. + /// Some SASL mechanisms, such as SCRAM-SHA1-PLUS and NTLM, are able to negotiate + /// channel-bindings. + /// + /// if channel-binding was negotiated; otherwise, . + public override bool NegotiatedChannelBinding { + get { return negotiatedChannelBinding; } + } + + /// + /// Get or set the desired channel-binding to be negotiated by the SASL mechanism. + /// + /// + /// Gets or sets the desired channel-binding to be negotiated by the SASL mechanism. + /// This value is optional. + /// + /// The type of channel-binding. + public ChannelBindingKind DesiredChannelBinding { + get; set; + } + + /// + /// Get whether or not the SASL mechanism supports negotiating a security layer. + /// + /// + /// Gets whether or not the SASL mechanism supports negotiating a security layer. + /// + /// if the SASL mechanism supports negotiating a security layer; otherwise, . + protected virtual bool SupportsSecurityLayer { + get { return false; } + } + + /// + /// Get whether or not a security layer was negotiated by the SASL mechanism. + /// + /// + /// Gets whether or not a security layer has been negotiated by the SASL mechanism. + /// Some SASL mechanisms, such as GSSAPI, are able to negotiate security layers + /// such as integrity and confidentiality protection. + /// + /// if a security layer was negotiated; otherwise, . + public override bool NegotiatedSecurityLayer { + get { return negotiatedSecurityLayer; } + } + + /// + /// Get or set the service principal name (SPN) of the service that the client wishes to authenticate with. + /// + /// + /// Get or set the service principal name (SPN) of the service that the client wishes to authenticate with. + /// This value is optional. + /// + /// The service principal name (SPN) of the service that the client wishes to authenticate with. + public string? ServicePrincipalName { + get; set; + } + + /// + /// Parse the server's challenge token and return the next challenge response. + /// + /// + /// Parses the server's challenge token and returns the next challenge response. + /// + /// The next challenge response. + /// The server's challenge token. + /// The index into the token specifying where the server's challenge begins. + /// The length of the server's challenge. + /// The cancellation token. + /// + /// The SASL mechanism does not support SASL-IR. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An error has occurred while parsing the server's challenge token. + /// + protected override byte[]? Challenge (byte[]? token, int startIndex, int length, CancellationToken cancellationToken = default) + { + if (!SupportsSecurityLayer && IsAuthenticated) + return null; + + cancellationToken.ThrowIfCancellationRequested (); + + // On the first call, initialize the NegotiateAuthentication if needed. + negotiate ??= new NegotiateAuthentication (CreateClientOptions ()); + + var challenge = token != null ? token.AsSpan (startIndex, length) : ReadOnlySpan.Empty; + + if (IsAuthenticated) { + // If auth completed and another challenge was received, then the server + // may be doing "correct" form of GSSAPI SASL. Validate the incoming and + // produce outgoing SASL security layer negotiate message. + return GetSecurityLayerNegotiationResponse (challenge); + } + + // Calculate the challenge response. + return GetChallengeResponse (challenge); + } + + /// + /// Create the . + /// + /// + /// Creates the . + /// + /// The client options. + protected virtual NegotiateAuthenticationClientOptions CreateClientOptions () + { + var options = new NegotiateAuthenticationClientOptions { + RequiredProtectionLevel = RequiredProtectionLevel, + Credential = Credentials, + Package = AuthMechanism, + }; + + if (DesiredChannelBinding != ChannelBindingKind.Unknown && TryGetChannelBinding (DesiredChannelBinding, out var channelBinding)) { + options.Binding = channelBinding; + requestedChannelBinding = true; + } + + if (!string.IsNullOrEmpty (ServicePrincipalName)) + options.TargetName = ServicePrincipalName; + + return options; + } + + byte[]? GetChallengeResponse (ReadOnlySpan challenge) + { + var response = negotiate!.GetOutgoingBlob (challenge, out NegotiateAuthenticationStatusCode statusCode); + + switch (statusCode) { + case NegotiateAuthenticationStatusCode.Completed: + // Authentication is completed (but may receive a Security Layer negotiation challenge next). + negotiatedChannelBinding = requestedChannelBinding; + IsAuthenticated = true; + break; + case NegotiateAuthenticationStatusCode.ContinueNeeded: + break; + default: + throw GetSaslException (MechanismName, statusCode); + } + + return response; + } + + // Function for SASL security layer negotiation after authorization completes. + // + // Returns null for failure. + // + // Cloned from: https://github.com/dotnet/runtime/blob/4631ecec883a90ae9c29c058eea4527f9f2cb473/src/libraries/System.Net.Mail/src/System/Net/Mail/SmtpNegotiateAuthenticationModule.cs#L107 + byte[]? GetSecurityLayerNegotiationResponse (ReadOnlySpan challenge) + { + NegotiateAuthenticationStatusCode statusCode; + byte[] input = challenge.ToArray (); + Span unwrapped; + + statusCode = negotiate!.UnwrapInPlace (input, out int unwrappedOffset, out int unwrappedLength, out _); + if (statusCode != NegotiateAuthenticationStatusCode.Completed) + return null; + + unwrapped = input.AsSpan (unwrappedOffset, unwrappedLength); + + // Per RFC 2222 Section 7.2.2: + // the client should then expect the server to issue a + // token in a subsequent challenge. The client passes + // this token to GSS_Unwrap and interprets the first + // octet of cleartext as a bit-mask specifying the + // security layers supported by the server and the + // second through fourth octets as the maximum size + // output_message to send to the server. + // Section 7.2.3 + // The security layer and their corresponding bit-masks + // are as follows: + // 1 No security layer + // 2 Integrity protection + // Sender calls GSS_Wrap with conf_flag set to FALSE + // 4 Privacy protection + // Sender calls GSS_Wrap with conf_flag set to TRUE + // + // Exchange 2007 and our client only support + // "No security layer". We verify that the server offers + // option to use no security layer and negotiate that if + // possible. + + if (unwrapped.Length != 4 || (unwrapped[0] & 0x01) != 0x01) + return null; + + // Continuing with RFC 2222 section 7.2.2: + // The client then constructs data, with the first octet + // containing the bit-mask specifying the selected security + // layer, the second through fourth octets containing in + // network byte order the maximum size output_message the client + // is able to receive, and the remaining octets containing the + // authorization identity. + // + // So now this constructs the "wrapped" response. + + // let MakeSignature figure out length of output + ArrayBufferWriter writer = new ArrayBufferWriter (); + statusCode = negotiate.Wrap (SaslNoSecurityLayerToken, writer, false, out _); + if (statusCode != NegotiateAuthenticationStatusCode.Completed) + return null; + + negotiatedSecurityLayer = true; + + return writer.WrittenSpan.ToArray (); + } + + /// + /// Reset the state of the SASL mechanism. + /// + /// + /// Resets the state of the SASL mechanism. + /// + public override void Reset () + { + if (negotiate != null) { + negotiatedChannelBinding = false; + requestedChannelBinding = false; + negotiatedSecurityLayer = false; + negotiate.Dispose (); + negotiate = null; + } + + base.Reset (); + } + + internal static bool CheckSupported (string mechanismName) + { + try { + var options = new NegotiateAuthenticationClientOptions { + Credential = new NetworkCredential ("username", "password"), + Package = mechanismName, + }; + NegotiateAuthenticationStatusCode statusCode; + + using (var negotiate = new NegotiateAuthentication (options)) + negotiate.GetOutgoingBlob (Array.Empty (), out statusCode); + + return statusCode == NegotiateAuthenticationStatusCode.Completed || + statusCode == NegotiateAuthenticationStatusCode.ContinueNeeded; + } catch { + return false; + } + } + } +} + +#endif // NET7_0_OR_GREATER diff --git a/MailKit/Security/SaslMechanismNtlm.cs b/MailKit/Security/SaslMechanismNtlm.cs index 1ab7b4842c..591eaacf54 100644 --- a/MailKit/Security/SaslMechanismNtlm.cs +++ b/MailKit/Security/SaslMechanismNtlm.cs @@ -1,9 +1,9 @@ -// +// // SaslMechanismNtlm.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -24,8 +24,12 @@ // THE SOFTWARE. // +// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/b38c36ed-2804-4868-a9ff-8dd3182128e4 + using System; using System.Net; +using System.Threading; +using System.Security.Authentication.ExtendedProtection; using MailKit.Security.Ntlm; @@ -34,59 +38,211 @@ namespace MailKit.Security { /// The NTLM SASL mechanism. /// /// - /// A SASL mechanism based on NTLM. + /// A SASL mechanism based on NTLM. + /// + /// NTLM is a legacy challenge-response authentication mechanism introduced by Microsoft + /// in the 1990's and suffers from the following weaknesses: + /// + /// Pass-the-Hash Attacks: Stolen NTLM hashes can be reused without knowing the password. + /// Relay Attacks: NTLM does not protect against credential forwarding. + /// Cryptography: NTLMv1 relies on DES and MD4 which are both very weak. NTLMv2 relies on HMAC-MD5 + /// which is better but still considered very weak by modern standards. + /// + /// Microsoft recommends disabling NTLM and migrating to Kerberos + /// (GSSAPI) + /// or modern alternatives. + /// /// public class SaslMechanismNtlm : SaslMechanism { + static readonly Version DefaultOSVersion; + enum LoginState { - Initial, + Negotiate, Challenge } + NtlmNegotiateMessage? negotiate; + bool negotiatedChannelBinding; LoginState state; + static SaslMechanismNtlm () + { + if (Environment.OSVersion.Platform == PlatformID.Win32NT) + DefaultOSVersion = Environment.OSVersion.Version; + else + DefaultOSVersion = new Version (10, 0, 22000, 0); + } + +#if NET48_OR_GREATER || NET5_0_OR_GREATER || NETSTANDARD2_0_OR_GREATER + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new NTLM SASL context using the default network credentials. + /// + public SaslMechanismNtlm () : this (CredentialCache.DefaultNetworkCredentials) + { + } +#endif + /// /// Initializes a new instance of the class. /// /// /// Creates a new NTLM SASL context. /// - /// The URI of the service. /// The user's credentials. /// - /// is null. + /// is . + /// + public SaslMechanismNtlm (NetworkCredential credentials) : base (credentials) + { + OSVersion = DefaultOSVersion; + Workstation = Environment.MachineName; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new NTLM SASL context. + /// + /// The user name. + /// The password. + /// + /// is . /// -or- - /// is null. + /// is . /// - public SaslMechanismNtlm (Uri uri, ICredentials credentials) : base (uri, credentials) + public SaslMechanismNtlm (string userName, string password) : base (userName, password) { + OSVersion = DefaultOSVersion; + Workstation = Environment.MachineName; + } + + /// + /// This is only used for unit testing purposes. + /// + internal byte[]? Nonce { + get; set; + } + + /// + /// This is only used for unit testing purposes. + /// + internal long? Timestamp { + get; set; } /// - /// Gets the name of the mechanism. + /// Get the name of the SASL mechanism. /// /// - /// Gets the name of the mechanism. + /// Gets the name of the SASL mechanism. /// - /// The name of the mechanism. + /// The name of the SASL mechanism. public override string MechanismName { get { return "NTLM"; } } /// - /// Gets whether or not the mechanism supports an initial response (SASL-IR). + /// Get whether or not the SASL mechanism supports channel binding. /// /// - /// SASL mechanisms that support sending an initial client response to the server - /// should return true. + /// Gets whether or not the SASL mechanism supports channel binding. /// - /// true if the mechanism supports an initial response; otherwise, false. + /// if the SASL mechanism supports channel binding; otherwise, . + public override bool SupportsChannelBinding { + get { return true; } + } + + /// + /// Get whether or not channel-binding was negotiated by the SASL mechanism. + /// + /// + /// Gets whether or not channel-binding has been negotiated by the SASL mechanism. + /// Some SASL mechanisms, such as SCRAM-SHA1-PLUS and NTLM, are able to negotiate + /// channel-bindings. + /// + /// if channel-binding was negotiated; otherwise, . + public override bool NegotiatedChannelBinding { + get { return negotiatedChannelBinding; } + } + + /// + /// Get whether or not the mechanism supports an initial response (SASL-IR). + /// + /// + /// Gets whether or not the mechanism supports an initial response (SASL-IR). + /// SASL mechanisms that support sending an initial client response to the server + /// should return . + /// + /// if the mechanism supports an initial response; otherwise, . public override bool SupportsInitialResponse { get { return true; } } /// - /// Parses the server's challenge token and returns the next challenge response. + /// Get or set a value indicating whether or not the NTLM SASL mechanism should allow channel-binding. + /// + /// + /// Gets or sets a value indicating whether or not the NTLM SASL mechanism should allow channel-binding. + /// In the future, this option will disappear as channel-binding will become the default. For now, + /// it is only an option because this feature has not been thoroughly tested. + /// + /// if the NTLM SASL mechanism should allow channel-binding; otherwise, . + public bool AllowChannelBinding { + get; set; + } + + /// + /// Get or set the Windows OS version to use in the NTLM negotiation (used for debugging purposes). + /// + /// + /// Gets or sets the Windows OS version to use in the NTLM negotiation (used for debugging purposes). + /// + /// The Windows OS version. + public Version OSVersion { + get; set; + } + + /// + /// Get or set the workstation name to use for authentication. + /// + /// + /// Gets or sets the workstation name to use for authentication. + /// + /// The workstation name. + public string Workstation { + get; set; + } + + /// + /// Get or set the service principal name (SPN) of the service that the client wishes to authenticate with. + /// + /// + /// Get or set the service principal name (SPN) of the service that the client wishes to authenticate with. + /// This value is optional. + /// + /// The service principal name (SPN) of the service that the client wishes to authenticate with. + public string? ServicePrincipalName { + get; set; + } + + /// + /// Get or set a value indicating that the caller generated the target's SPN from an untrusted source. + /// + /// + /// Gets or sets a value indicating that the caller generated the target's SPN from an untrusted source. + /// + /// if the is unverified; otherwise, . + public bool IsUnverifiedServicePrincipalName { + get; set; + } + + /// + /// Parse the server's challenge token and return the next challenge response. /// /// /// Parses the server's challenge token and returns the next challenge response. @@ -95,74 +251,97 @@ public override bool SupportsInitialResponse { /// The server's challenge token. /// The index into the token specifying where the server's challenge begins. /// The length of the server's challenge. - /// - /// The SASL mechanism is already authenticated. + /// The cancellation token. + /// + /// The SASL mechanism does not support SASL-IR. + /// + /// + /// The operation was canceled via the cancellation token. /// /// /// An error has occurred while parsing the server's challenge token. /// - protected override byte[] Challenge (byte[] token, int startIndex, int length) + protected override byte[]? Challenge (byte[]? token, int startIndex, int length, CancellationToken cancellationToken) { if (IsAuthenticated) - throw new InvalidOperationException (); + return null; - var cred = Credentials.GetCredential (Uri, MechanismName); - string password = cred.Password ?? string.Empty; - string userName = cred.UserName; - string domain = cred.Domain; - MessageBase message; + string userName = Credentials.UserName; + string domain = Credentials.Domain; + NtlmMessageBase? message = null; if (string.IsNullOrEmpty (domain)) { - int index = userName.IndexOf ('\\'); - if (index == -1) - index = userName.IndexOf ('/'); + int index; + + if ((index = userName.LastIndexOf ('@')) != -1) { + domain = userName.Substring (index + 1); + userName = userName.Substring (0, index); + } else { + if ((index = userName.IndexOf ('\\')) == -1) + index = userName.IndexOf ('/'); - if (index >= 0) { - domain = userName.Substring (0, index); - userName = userName.Substring (index + 1); + if (index >= 0) { + domain = userName.Substring (0, index); + userName = userName.Substring (index + 1); + } } } switch (state) { - case LoginState.Initial: - message = GetInitialResponse (domain); + case LoginState.Negotiate: + message = negotiate = new NtlmNegotiateMessage (domain, Workstation, OSVersion); state = LoginState.Challenge; break; case LoginState.Challenge: - message = GetChallengeResponse (userName, password, domain, token, startIndex, length); + if (token == null) + throw new SaslException (MechanismName, SaslErrorCode.MissingChallenge, "Server response did not contain any authentication data."); + + var password = Credentials.Password; + message = GetChallengeResponse (domain, userName, password, token, startIndex, length); IsAuthenticated = true; break; - default: - throw new IndexOutOfRangeException ("state"); } - return message.Encode (); + return message?.Encode (); } - static MessageBase GetInitialResponse (string domain) + NtlmAuthenticateMessage GetChallengeResponse (string domain, string userName, string password, byte[] token, int startIndex, int length) { - return new Type1Message (string.Empty, domain); - } + var challenge = new NtlmChallengeMessage (token, startIndex, length); + var authenticate = new NtlmAuthenticateMessage (negotiate!, challenge, userName, password, domain, Workstation) { + ClientChallenge = Nonce, + Timestamp = Timestamp + }; + byte[]? channelBindingToken = null; - static MessageBase GetChallengeResponse (string userName, string password, string domain, byte[] token, int startIndex, int length) - { - var type2 = new Type2Message (token, startIndex, length); - var type3 = new Type3Message (type2, userName, string.Empty); - type3.Password = password; - type3.Domain = domain; + if (AllowChannelBinding && challenge.TargetInfo != null) { + // Only bother with attempting to channel-bind if the CHALLENGE_MESSAGE's TargetInfo is not NULL. + // Not sure which channel-binding types are supported by NTLM, but I am told that supposedly the + // System.Net.Mail.SmtpClient uses tls-unique, so we'll go with that... + negotiatedChannelBinding = TryGetChannelBindingToken (ChannelBindingKind.Endpoint, out channelBindingToken); + } + + authenticate.ComputeNtlmV2 (ServicePrincipalName, IsUnverifiedServicePrincipalName, channelBindingToken); + + if (channelBindingToken != null) + Array.Clear (channelBindingToken, 0, channelBindingToken.Length); - return type3; + negotiate = null; + + return authenticate; } /// - /// Resets the state of the SASL mechanism. + /// Reset the state of the SASL mechanism. /// /// /// Resets the state of the SASL mechanism. /// public override void Reset () { - state = LoginState.Initial; + negotiatedChannelBinding = false; + state = LoginState.Negotiate; + negotiate = null; base.Reset (); } } diff --git a/MailKit/Security/SaslMechanismNtlmNative.cs b/MailKit/Security/SaslMechanismNtlmNative.cs new file mode 100644 index 0000000000..df87fe9da5 --- /dev/null +++ b/MailKit/Security/SaslMechanismNtlmNative.cs @@ -0,0 +1,120 @@ +// +// SaslMechanismNtlmNative.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +#if NET7_0_OR_GREATER + +using System.Net; +using System.Net.Security; + +namespace MailKit.Security { + /// + /// The NTLM SASL mechanism. + /// + /// + /// A SASL mechanism based on NTLM that uses .NET Core's class for authenticating. + /// + /// NTLM is a legacy challenge-response authentication mechanism introduced by Microsoft + /// in the 1990's and suffers from the following weaknesses: + /// + /// Pass-the-Hash Attacks: Stolen NTLM hashes can be reused without knowing the password. + /// Relay Attacks: NTLM does not protect against credential forwarding. + /// Cryptography: NTLMv1 relies on DES and MD4 which are both very weak. NTLMv2 relies on HMAC-MD5 + /// which is better but still considered very weak by modern standards. + /// + /// Microsoft recommends disabling NTLM and migrating to Kerberos + /// (GSSAPI) + /// or modern alternatives. + /// + /// + public class SaslMechanismNtlmNative : SaslMechanismNegotiateBase + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new SASL context using the default network credentials. + /// + public SaslMechanismNtlmNative () : this (CredentialCache.DefaultNetworkCredentials) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new NTLM SASL context. + /// + /// The user's credentials. + /// + /// is . + /// + public SaslMechanismNtlmNative (NetworkCredential credentials) : base (credentials) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new NTLM SASL context. + /// + /// The user name. + /// The password. + /// + /// is . + /// -or- + /// is . + /// + public SaslMechanismNtlmNative (string userName, string password) : base (userName, password) + { + } + + /// + /// Get the name of the authentication mechanism. + /// + /// + /// Gets the name of the authentication mechanism. + /// This value MUST be one of the following: "NTLM", "Kerberos" or "Negotiate". + /// + /// The name of the authentication mechanism. + protected override string AuthMechanism { + get { return MechanismName; } + } + + /// + /// Get the name of the SASL mechanism. + /// + /// + /// Gets the name of the SASL mechanism. + /// + /// The name of the SASL mechanism. + public override string MechanismName { + get { return "NTLM"; } + } + } +} + +#endif // NET7_0_OR_GREATER diff --git a/MailKit/Security/SaslMechanismOAuth2.cs b/MailKit/Security/SaslMechanismOAuth2.cs index b3015686d7..4305b70a4f 100644 --- a/MailKit/Security/SaslMechanismOAuth2.cs +++ b/MailKit/Security/SaslMechanismOAuth2.cs @@ -1,9 +1,9 @@ -// +// // SaslMechanismOAuth2.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -26,6 +26,7 @@ using System; using System.Net; +using System.Threading; namespace MailKit.Security { /// @@ -35,6 +36,10 @@ namespace MailKit.Security { /// A SASL mechanism used by Google that makes use of a short-lived /// OAuth 2.0 access token. /// + /// + /// + /// + /// public class SaslMechanismOAuth2 : SaslMechanism { const string AuthBearer = "auth=Bearer "; @@ -46,42 +51,61 @@ public class SaslMechanismOAuth2 : SaslMechanism /// /// Creates a new XOAUTH2 SASL context. /// - /// The URI of the service. /// The user's credentials. /// - /// is null. + /// is . + /// + public SaslMechanismOAuth2 (NetworkCredential credentials) : base (credentials) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new XOAUTH2 SASL context. + /// + /// + /// + /// + /// + /// The user name. + /// The auth token. + /// + /// is . /// -or- - /// is null. + /// is . /// - public SaslMechanismOAuth2 (Uri uri, ICredentials credentials) : base (uri, credentials) + public SaslMechanismOAuth2 (string userName, string auth_token) : base (userName, auth_token) { } /// - /// Gets the name of the mechanism. + /// Get the name of the SASL mechanism. /// /// - /// Gets the name of the mechanism. + /// Gets the name of the SASL mechanism. /// - /// The name of the mechanism. + /// The name of the SASL mechanism. public override string MechanismName { get { return "XOAUTH2"; } } /// - /// Gets whether or not the mechanism supports an initial response (SASL-IR). + /// Get whether or not the mechanism supports an initial response (SASL-IR). /// /// - /// SASL mechanisms that support sending an initial client response to the server - /// should return true. + /// Gets whether or not the mechanism supports an initial response (SASL-IR). + /// SASL mechanisms that support sending an initial client response to the server + /// should return . /// - /// true if the mechanism supports an initial response; otherwise, false. + /// if the mechanism supports an initial response; otherwise, . public override bool SupportsInitialResponse { get { return true; } } /// - /// Parses the server's challenge token and returns the next challenge response. + /// Parse the server's challenge token and return the next challenge response. /// /// /// Parses the server's challenge token and returns the next challenge response. @@ -90,20 +114,23 @@ public override bool SupportsInitialResponse { /// The server's challenge token. /// The index into the token specifying where the server's challenge begins. /// The length of the server's challenge. - /// - /// The SASL mechanism is already authenticated. + /// The cancellation token. + /// + /// The SASL mechanism does not support SASL-IR. + /// + /// + /// The operation was canceled via the cancellation token. /// /// /// An error has occurred while parsing the server's challenge token. /// - protected override byte[] Challenge (byte[] token, int startIndex, int length) + protected override byte[]? Challenge (byte[]? token, int startIndex, int length, CancellationToken cancellationToken) { if (IsAuthenticated) - throw new InvalidOperationException (); + return null; - var cred = Credentials.GetCredential (Uri, MechanismName); - var authToken = cred.Password; - var userName = cred.UserName; + var authToken = Credentials.Password; + var userName = Credentials.UserName; int index = 0; var buf = new byte[UserEquals.Length + userName.Length + AuthBearer.Length + authToken.Length + 3]; diff --git a/MailKit/Security/SaslMechanismOAuthBearer.cs b/MailKit/Security/SaslMechanismOAuthBearer.cs new file mode 100644 index 0000000000..4149f21f8a --- /dev/null +++ b/MailKit/Security/SaslMechanismOAuthBearer.cs @@ -0,0 +1,205 @@ +// +// SaslMechanismOAuthBearer.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Net; +using System.Text; +using System.Threading; +using System.Globalization; + +namespace MailKit.Security { + /// + /// The OAuth Bearer SASL mechanism. + /// + /// + /// A SASL mechanism that makes use of a short-lived OAuth Bearer access tokens. + /// + /// + /// + /// + /// + public class SaslMechanismOAuthBearer : SaslMechanism + { + static readonly byte[] ErrorResponse = new byte[1] { 0x01 }; + const string AuthBearer = "auth=Bearer "; + const string HostEquals = "host="; + const string PortEquals = "port="; + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new OAUTHBEARER SASL context. + /// + /// The user's credentials. + /// + /// is . + /// + public SaslMechanismOAuthBearer (NetworkCredential credentials) : base (credentials) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new OAUTHBEARER SASL context. + /// + /// + /// + /// + /// + /// The user name. + /// The auth token. + /// + /// is . + /// -or- + /// is . + /// + public SaslMechanismOAuthBearer (string userName, string auth_token) : base (userName, auth_token) + { + } + + /// + /// Get the name of the SASL mechanism. + /// + /// + /// Gets the name of the SASL mechanism. + /// + /// The name of the SASL mechanism. + public override string MechanismName { + get { return "OAUTHBEARER"; } + } + + /// + /// Get whether or not the mechanism supports an initial response (SASL-IR). + /// + /// + /// Gets whether or not the mechanism supports an initial response (SASL-IR). + /// SASL mechanisms that support sending an initial client response to the server + /// should return . + /// + /// if the mechanism supports an initial response; otherwise, . + public override bool SupportsInitialResponse { + get { return true; } + } + + static int CalculateBufferSize (byte[] authzid, byte[] host, string port, string token) + { + int length = 0; + + length += 2; // channel binding ("n,") + length += 2; // a= + length += authzid.Length; + length += 1; // ',' + + length++; // ^A + + length += HostEquals.Length; + length += host.Length; + length++; // ^A + + length += PortEquals.Length; + length += port.Length; + length++; // ^A + + length += AuthBearer.Length; + length += token.Length; + length += 2; // ^A^A + + return length; + } + + /// + /// Parse the server's challenge token and return the next challenge response. + /// + /// + /// Parses the server's challenge token and returns the next challenge response. + /// + /// The next challenge response. + /// The server's challenge token. + /// The index into the token specifying where the server's challenge begins. + /// The length of the server's challenge. + /// The cancellation token. + /// + /// The SASL mechanism does not support SASL-IR. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An error has occurred while parsing the server's challenge token. + /// + protected override byte[]? Challenge (byte[]? token, int startIndex, int length, CancellationToken cancellationToken) + { + if (IsAuthenticated) + return ErrorResponse; + + if (Uri is null) + throw new InvalidOperationException (); + + var authzid = Encoding.UTF8.GetBytes (Credentials.UserName); + var port = Uri.Port.ToString (CultureInfo.InvariantCulture); + var host = Encoding.UTF8.GetBytes (Uri.Host); + var authToken = Credentials.Password; + + var buf = new byte[CalculateBufferSize (authzid, host, port, authToken)]; + int index = 0; + + buf[index++] = (byte) 'n'; // channel binding not supported + buf[index++] = (byte) ','; + buf[index++] = (byte) 'a'; + buf[index++] = (byte) '='; + for (int i = 0; i < authzid.Length; i++) + buf[index++] = authzid[i]; + buf[index++] = (byte) ','; + buf[index++] = 0x01; + + for (int i = 0; i < HostEquals.Length; i++) + buf[index++] = (byte) HostEquals[i]; + for (int i = 0; i < host.Length; i++) + buf[index++] = host[i]; + buf[index++] = 0x01; + + for (int i = 0; i < PortEquals.Length; i++) + buf[index++] = (byte) PortEquals[i]; + for (int i = 0; i < port.Length; i++) + buf[index++] = (byte) port[i]; + buf[index++] = 0x01; + + for (int i = 0; i < AuthBearer.Length; i++) + buf[index++] = (byte) AuthBearer[i]; + for (int i = 0; i < authToken.Length; i++) + buf[index++] = (byte) authToken[i]; + buf[index++] = 0x01; + buf[index++] = 0x01; + + IsAuthenticated = true; + + return buf; + } + } +} diff --git a/MailKit/Security/SaslMechanismPlain.cs b/MailKit/Security/SaslMechanismPlain.cs index d0405c07a5..793ae36725 100644 --- a/MailKit/Security/SaslMechanismPlain.cs +++ b/MailKit/Security/SaslMechanismPlain.cs @@ -1,9 +1,9 @@ -// +// // SaslMechanismPlain.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -27,10 +27,7 @@ using System; using System.Net; using System.Text; - -#if NETFX_CORE -using Encoding = Portable.Text.Encoding; -#endif +using System.Threading; namespace MailKit.Security { /// @@ -44,7 +41,7 @@ namespace MailKit.Security { /// public class SaslMechanismPlain : SaslMechanism { - Encoding encoding; + readonly Encoding encoding; /// /// Initializes a new instance of the class. @@ -52,17 +49,38 @@ public class SaslMechanismPlain : SaslMechanism /// /// Creates a new PLAIN SASL context. /// - /// The URI of the service. /// The encoding to use for the user's credentials. /// The user's credentials. /// - /// is null. + /// is . + /// -or- + /// is . + /// + public SaslMechanismPlain (Encoding encoding, NetworkCredential credentials) : base (credentials) + { + if (encoding == null) + throw new ArgumentNullException (nameof (encoding)); + + this.encoding = encoding; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new PLAIN SASL context. + /// + /// The encoding to use for the user's credentials. + /// The user name. + /// The password. + /// + /// is . /// -or- - /// is null. + /// is . /// -or- - /// is null. + /// is . /// - public SaslMechanismPlain (Uri uri, Encoding encoding, ICredentials credentials) : base (uri, credentials) + public SaslMechanismPlain (Encoding encoding, string userName, string password) : base (userName, password) { if (encoding == null) throw new ArgumentNullException (nameof (encoding)); @@ -76,43 +94,69 @@ public SaslMechanismPlain (Uri uri, Encoding encoding, ICredentials credentials) /// /// Creates a new PLAIN SASL context. /// - /// The URI of the service. /// The user's credentials. /// - /// is null. + /// is . + /// + public SaslMechanismPlain (NetworkCredential credentials) : this (Encoding.UTF8, credentials) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new PLAIN SASL context. + /// + /// The user name. + /// The password. + /// + /// is . /// -or- - /// is null. + /// is . /// - public SaslMechanismPlain (Uri uri, ICredentials credentials) : base (uri, credentials) + public SaslMechanismPlain (string userName, string password) : this (Encoding.UTF8, userName, password) { - encoding = Encoding.UTF8; } /// - /// Gets the name of the mechanism. + /// Get or set the authorization identifier. /// /// - /// Gets the name of the mechanism. + /// The authorization identifier is the desired user account that the server should use + /// for all accesses. This is separate from the user name used for authentication. /// - /// The name of the mechanism. + /// The authorization identifier. + public string? AuthorizationId { + get; set; + } + + /// + /// Get the name of the SASL mechanism. + /// + /// + /// Gets the name of the SASL mechanism. + /// + /// The name of the SASL mechanism. public override string MechanismName { get { return "PLAIN"; } } /// - /// Gets whether or not the mechanism supports an initial response (SASL-IR). + /// Get whether or not the mechanism supports an initial response (SASL-IR). /// /// - /// SASL mechanisms that support sending an initial client response to the server - /// should return true. + /// Gets whether or not the mechanism supports an initial response (SASL-IR). + /// SASL mechanisms that support sending an initial client response to the server + /// should return . /// - /// true if the mechanism supports an initial response; otherwise, false. + /// if the mechanism supports an initial response; otherwise, . public override bool SupportsInitialResponse { get { return true; } } /// - /// Parses the server's challenge token and returns the next challenge response. + /// Parse the server's challenge token and return the next challenge response. /// /// /// Parses the server's challenge token and returns the next challenge response. @@ -121,30 +165,39 @@ public override bool SupportsInitialResponse { /// The server's challenge token. /// The index into the token specifying where the server's challenge begins. /// The length of the server's challenge. - /// - /// The SASL mechanism is already authenticated. + /// The cancellation token. + /// + /// The SASL mechanism does not support SASL-IR. + /// + /// + /// The operation was canceled via the cancellation token. /// /// /// An error has occurred while parsing the server's challenge token. /// - protected override byte[] Challenge (byte[] token, int startIndex, int length) + protected override byte[]? Challenge (byte[]? token, int startIndex, int length, CancellationToken cancellationToken) { if (IsAuthenticated) - throw new InvalidOperationException (); + return null; - var cred = Credentials.GetCredential (Uri, MechanismName); - var userName = encoding.GetBytes (cred.UserName); - var password = encoding.GetBytes (cred.Password); - var buffer = new byte[userName.Length + password.Length + 2]; + var authzid = encoding.GetBytes (AuthorizationId ?? string.Empty); + var authcid = encoding.GetBytes (Credentials.UserName); + var passwd = encoding.GetBytes (Credentials.Password); + var buffer = new byte[authzid.Length + authcid.Length + passwd.Length + 2]; int offset = 0; + for (int i = 0; i < authzid.Length; i++) + buffer[offset++] = authzid[i]; + buffer[offset++] = 0; - for (int i = 0; i < userName.Length; i++) - buffer[offset++] = userName[i]; + for (int i = 0; i < authcid.Length; i++) + buffer[offset++] = authcid[i]; buffer[offset++] = 0; - for (int i = 0; i < password.Length; i++) - buffer[offset++] = password[i]; + for (int i = 0; i < passwd.Length; i++) + buffer[offset++] = passwd[i]; + + Array.Clear (passwd, 0, passwd.Length); IsAuthenticated = true; diff --git a/MailKit/Security/SaslMechanismScramBase.cs b/MailKit/Security/SaslMechanismScramBase.cs index 35f2d5d3fb..3db45490f0 100644 --- a/MailKit/Security/SaslMechanismScramBase.cs +++ b/MailKit/Security/SaslMechanismScramBase.cs @@ -1,9 +1,9 @@ -// +// // SaslMechanismScramBase.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -27,13 +27,11 @@ using System; using System.Net; using System.Text; +using System.Threading; +using System.Globalization; using System.Collections.Generic; - -#if NETFX_CORE -using Encoding = Portable.Text.Encoding; -#else using System.Security.Cryptography; -#endif +using System.Security.Authentication.ExtendedProtection; namespace MailKit.Security { /// @@ -50,10 +48,13 @@ enum LoginState { Validate } - string client, server; - byte[] salted, auth; + ChannelBindingKind channelBindingKind; + bool negotiatedChannelBinding; + byte[]? channelBindingToken; + internal string? cnonce; + string? client, server; + byte[]? salted, auth; LoginState state; - string cnonce; /// /// Initializes a new instance of the class. @@ -61,17 +62,12 @@ enum LoginState { /// /// Creates a new SCRAM-based SASL context. /// - /// The URI of the service. /// The user's credentials. - /// Random characters to act as the cnonce token. /// - /// is null. - /// -or- - /// is null. + /// is . /// - internal protected SaslMechanismScramBase (Uri uri, ICredentials credentials, string entropy) : base (uri, credentials) + protected SaslMechanismScramBase (NetworkCredential credentials) : base (credentials) { - cnonce = entropy; } /// @@ -80,34 +76,64 @@ internal protected SaslMechanismScramBase (Uri uri, ICredentials credentials, st /// /// Creates a new SCRAM-based SASL context. /// - /// The URI of the service. - /// The user's credentials. + /// The user name. + /// The password. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// - protected SaslMechanismScramBase (Uri uri, ICredentials credentials) : base (uri, credentials) + protected SaslMechanismScramBase (string userName, string password) : base (userName, password) { } /// - /// Gets whether or not the mechanism supports an initial response (SASL-IR). + /// Get or set the authorization identifier. + /// + /// + /// The authorization identifier is the desired user account that the server should use + /// for all accesses. This is separate from the user name used for authentication. + /// + /// The authorization identifier. + public string? AuthorizationId { + get; set; + } + + /// + /// Get whether or not the mechanism supports an initial response (SASL-IR). /// /// - /// SASL mechanisms that support sending an initial client response to the server - /// should return true. + /// Get whether or not the mechanism supports an initial response (SASL-IR). + /// SASL mechanisms that support sending an initial client response to the server + /// should return . /// - /// true if the mechanism supports an initial response; otherwise, false. + /// if the mechanism supports an initial response; otherwise, . public override bool SupportsInitialResponse { get { return true; } } + /// + /// Get whether or not channel-binding was negotiated by the SASL mechanism. + /// + /// + /// Gets whether or not channel-binding has been negotiated by the SASL mechanism. + /// Some SASL mechanisms, such as SCRAM-SHA1-PLUS and NTLM, are able to negotiate + /// channel-bindings. + /// + /// if channel-binding was negotiated; otherwise, . + public override bool NegotiatedChannelBinding { + get { return negotiatedChannelBinding; } + } + static string Normalize (string str) { - var builder = new StringBuilder (); var prepared = SaslPrep (str); + if (prepared.Length == 0) + return prepared; + + var builder = new StringBuilder (); + for (int i = 0; i < prepared.Length; i++) { switch (prepared[i]) { case ',': builder.Append ("=2C"); break; @@ -232,8 +258,28 @@ static Dictionary ParseServerChallenge (string challenge) return results; } + static string GetChannelBindingName (ChannelBindingKind kind) + { + return kind == ChannelBindingKind.Endpoint ? "tls-server-end-point" : "tls-unique"; + } + + static string GetChannelBindingInput (ChannelBindingKind kind, string? authzid) + { + string flag; + + if (kind != ChannelBindingKind.Unknown) { + flag = "p=" + GetChannelBindingName (kind); + } else { + flag = "n"; + } + + authzid ??= string.Empty; + + return flag + "," + Normalize (authzid) + ","; + } + /// - /// Parses the server's challenge token and returns the next challenge response. + /// Parse the server's challenge token and return the next challenge response. /// /// /// Parses the server's challenge token and returns the next challenge response. @@ -242,39 +288,57 @@ static Dictionary ParseServerChallenge (string challenge) /// The server's challenge token. /// The index into the token specifying where the server's challenge begins. /// The length of the server's challenge. - /// - /// The SASL mechanism is already authenticated. + /// The cancellation token. + /// + /// The SASL mechanism does not support SASL-IR. + /// + /// + /// The operation was canceled via the cancellation token. /// /// /// An error has occurred while parsing the server's challenge token. /// - protected override byte[] Challenge (byte[] token, int startIndex, int length) + protected override byte[]? Challenge (byte[]? token, int startIndex, int length, CancellationToken cancellationToken) { if (IsAuthenticated) - throw new InvalidOperationException (); + return null; - var cred = Credentials.GetCredential (Uri, MechanismName); byte[] response, signature; + string input; switch (state) { case LoginState.Initial: - if (string.IsNullOrEmpty (cnonce)) { - var entropy = new byte[18]; - - using (var rng = RandomNumberGenerator.Create ()) - rng.GetBytes (entropy); - - cnonce = Convert.ToBase64String (entropy); + cnonce ??= GenerateEntropy (18); + client = "n=" + Normalize (Credentials.UserName) + ",r=" + cnonce; + + // Note: RFC7677 states: + // + // After publication of [RFC5802], it was discovered that Transport + // Layer Security (TLS) [RFC5246] does not have the expected properties + // for the "tls-unique" channel binding to be secure[RFC7627]. + // + // Based on this, we attempt to use "tls-server-end-point" instead of "tls-unique" when available. + if (SupportsChannelBinding) { + if (TryGetChannelBindingToken (ChannelBindingKind.Endpoint, out channelBindingToken)) { + channelBindingKind = ChannelBindingKind.Endpoint; + } else if (TryGetChannelBindingToken (ChannelBindingKind.Unique, out channelBindingToken)) { + channelBindingKind = ChannelBindingKind.Unique; + } else { + channelBindingKind = ChannelBindingKind.Unknown; + } } - client = "n=" + Normalize (cred.UserName) + ",r=" + cnonce; - response = Encoding.UTF8.GetBytes ("n,," + client); + input = GetChannelBindingInput (channelBindingKind, AuthorizationId); + response = Encoding.UTF8.GetBytes (input + client); state = LoginState.Final; break; case LoginState.Final: + if (token == null) + throw new SaslException (MechanismName, SaslErrorCode.MissingChallenge, "Server response did not contain any authentication data."); + server = Encoding.UTF8.GetString (token, startIndex, length); var tokens = ParseServerChallenge (server); - string salt, nonce, iterations; + string? salt, nonce, iterations; int count; if (!tokens.TryGetValue ('s', out salt)) @@ -286,16 +350,37 @@ protected override byte[] Challenge (byte[] token, int startIndex, int length) if (!tokens.TryGetValue ('i', out iterations)) throw new SaslException (MechanismName, SaslErrorCode.IncompleteChallenge, "Challenge did not contain an iteration count."); - if (!nonce.StartsWith (cnonce, StringComparison.Ordinal)) + if (!nonce.StartsWith (cnonce!, StringComparison.Ordinal)) throw new SaslException (MechanismName, SaslErrorCode.InvalidChallenge, "Challenge contained an invalid nonce."); - if (!int.TryParse (iterations, out count) || count < 1) + if (!int.TryParse (iterations, NumberStyles.None, CultureInfo.InvariantCulture, out count) || count < 1) throw new SaslException (MechanismName, SaslErrorCode.InvalidChallenge, "Challenge contained an invalid iteration count."); - var password = Encoding.UTF8.GetBytes (SaslPrep (cred.Password)); + var password = Encoding.UTF8.GetBytes (SaslPrep (Credentials.Password)); salted = Hi (password, Convert.FromBase64String (salt), count); + Array.Clear (password, 0, password.Length); + + input = GetChannelBindingInput (channelBindingKind, AuthorizationId); + var inputBuffer = Encoding.ASCII.GetBytes (input); + string base64; + + if (SupportsChannelBinding && channelBindingToken != null) { + var binding = new byte[inputBuffer.Length + channelBindingToken.Length]; + + Buffer.BlockCopy (inputBuffer, 0, binding, 0, inputBuffer.Length); + Buffer.BlockCopy (channelBindingToken, 0, binding, inputBuffer.Length, channelBindingToken.Length); + + // Zero the channel binding token. We don't need it anymore. + Array.Clear (channelBindingToken, 0, channelBindingToken.Length); + channelBindingToken = null; + + base64 = Convert.ToBase64String (binding); + } else { + base64 = Convert.ToBase64String (inputBuffer); + } + + var withoutProof = "c=" + base64 + ",r=" + nonce; - var withoutProof = "c=" + Convert.ToBase64String (Encoding.ASCII.GetBytes ("n,,")) + ",r=" + nonce; auth = Encoding.UTF8.GetBytes (client + "," + server + "," + withoutProof); var key = HMAC (salted, Encoding.ASCII.GetBytes ("Client Key")); @@ -306,25 +391,29 @@ protected override byte[] Challenge (byte[] token, int startIndex, int length) state = LoginState.Validate; break; case LoginState.Validate: + if (token == null) + throw new SaslException (MechanismName, SaslErrorCode.MissingChallenge, "Server response did not contain any authentication data."); + var challenge = Encoding.UTF8.GetString (token, startIndex, length); if (!challenge.StartsWith ("v=", StringComparison.Ordinal)) throw new SaslException (MechanismName, SaslErrorCode.InvalidChallenge, "Challenge did not start with a signature."); signature = Convert.FromBase64String (challenge.Substring (2)); - var serverKey = HMAC (salted, Encoding.ASCII.GetBytes ("Server Key")); - var calculated = HMAC (serverKey, auth); + var serverKey = HMAC (salted!, Encoding.ASCII.GetBytes ("Server Key")); + var calculated = HMAC (serverKey, auth!); if (signature.Length != calculated.Length) throw new SaslException (MechanismName, SaslErrorCode.IncorrectHash, "Challenge contained a signature with an invalid length."); for (int i = 0; i < signature.Length; i++) { if (signature[i] != calculated[i]) - throw new SaslException (MechanismName, SaslErrorCode.IncorrectHash, "Challenge contained an invalid signatire."); + throw new SaslException (MechanismName, SaslErrorCode.IncorrectHash, $"Challenge contained an invalid signature. Expected: {Convert.ToBase64String (calculated)}"); } + negotiatedChannelBinding = channelBindingKind != ChannelBindingKind.Unknown; IsAuthenticated = true; - response = new byte[0]; + response = Array.Empty (); break; default: throw new IndexOutOfRangeException ("state"); @@ -334,13 +423,20 @@ protected override byte[] Challenge (byte[] token, int startIndex, int length) } /// - /// Resets the state of the SASL mechanism. + /// Reset the state of the SASL mechanism. /// /// /// Resets the state of the SASL mechanism. /// public override void Reset () { + if (channelBindingToken != null) { + Array.Clear (channelBindingToken, 0, channelBindingToken.Length); + channelBindingToken = null; + } + + channelBindingKind = ChannelBindingKind.Unknown; + negotiatedChannelBinding = false; state = LoginState.Initial; client = null; server = null; diff --git a/MailKit/Security/SaslMechanismScramSha1.cs b/MailKit/Security/SaslMechanismScramSha1.cs index 4c0487c3c0..bc58f36fac 100644 --- a/MailKit/Security/SaslMechanismScramSha1.cs +++ b/MailKit/Security/SaslMechanismScramSha1.cs @@ -1,9 +1,9 @@ -// +// // SaslMechanismScramSha1.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -26,13 +26,7 @@ using System; using System.Net; - -#if NETFX_CORE -using Windows.Security.Cryptography; -using Windows.Security.Cryptography.Core; -#else using System.Security.Cryptography; -#endif namespace MailKit.Security { /// @@ -49,15 +43,11 @@ public class SaslMechanismScramSha1 : SaslMechanismScramBase /// /// Creates a new SCRAM-SHA-1 SASL context. /// - /// The URI of the service. /// The user's credentials. - /// Random characters to act as the cnonce token. /// - /// is null. - /// -or- - /// is null. + /// is . /// - internal SaslMechanismScramSha1 (Uri uri, ICredentials credentials, string entropy) : base (uri, credentials, entropy) + public SaslMechanismScramSha1 (NetworkCredential credentials) : base (credentials) { } @@ -67,19 +57,19 @@ internal SaslMechanismScramSha1 (Uri uri, ICredentials credentials, string entro /// /// Creates a new SCRAM-SHA-1 SASL context. /// - /// The URI of the service. - /// The user's credentials. + /// The user name. + /// The password. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// - public SaslMechanismScramSha1 (Uri uri, ICredentials credentials) : base (uri, credentials) + public SaslMechanismScramSha1 (string userName, string password) : base (userName, password) { } /// - /// Gets the name of the mechanism. + /// Get the name of the mechanism. /// /// /// Gets the name of the mechanism. @@ -115,18 +105,70 @@ protected override KeyedHashAlgorithm CreateHMAC (byte[] key) /// The string. protected override byte[] Hash (byte[] str) { -#if NETFX_CORE - var sha1 = HashAlgorithmProvider.OpenAlgorithm (HashAlgorithmNames.Sha1); - var buf = sha1.HashData (CryptographicBuffer.CreateFromByteArray (str)); - byte[] hash; - - CryptographicBuffer.CopyToByteArray (buf, out hash); - - return hash; -#else using (var sha1 = SHA1.Create ()) return sha1.ComputeHash (str); -#endif + } + } + + /// + /// The SCRAM-SHA-1-PLUS SASL mechanism. + /// + /// + /// A salted challenge/response SASL mechanism that uses the HMAC SHA-1 algorithm and Transport Layer Security (TLS) channel binding. + /// + public class SaslMechanismScramSha1Plus : SaslMechanismScramSha1 + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new SCRAM-SHA-1-PLUS SASL context. + /// + /// The user's credentials. + /// + /// is . + /// + public SaslMechanismScramSha1Plus (NetworkCredential credentials) : base (credentials) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new SCRAM-SHA-1-PLUS SASL context. + /// + /// The user name. + /// The password. + /// + /// is . + /// -or- + /// is . + /// + public SaslMechanismScramSha1Plus (string userName, string password) : base (userName, password) + { + } + + /// + /// Get the name of the SASL mechanism. + /// + /// + /// Gets the name of the SASL mechanism. + /// + /// The name of the SASL mechanism. + public override string MechanismName { + get { return "SCRAM-SHA-1-PLUS"; } + } + + /// + /// Get whether or not the SASL mechanism supports channel binding. + /// + /// + /// Gets whether or not the SASL mechanism supports channel binding. + /// + /// if the SASL mechanism supports channel binding; otherwise, . + public override bool SupportsChannelBinding { + get { return true; } } } } diff --git a/MailKit/Security/SaslMechanismScramSha256.cs b/MailKit/Security/SaslMechanismScramSha256.cs index 90936a7777..a78d452917 100644 --- a/MailKit/Security/SaslMechanismScramSha256.cs +++ b/MailKit/Security/SaslMechanismScramSha256.cs @@ -1,9 +1,9 @@ -// +// // SaslMechanismScramSha256.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -26,21 +26,11 @@ using System; using System.Net; - -#if __MOBILE__ -using SHA256CryptoServiceProvider = System.Security.Cryptography.SHA256Managed; -#endif - -#if NETFX_CORE -using Windows.Security.Cryptography; -using Windows.Security.Cryptography.Core; -#else using System.Security.Cryptography; -#endif namespace MailKit.Security { /// - /// The SCRAM-SHA-1 SASL mechanism. + /// The SCRAM-SHA-256 SASL mechanism. /// /// /// A salted challenge/response SASL mechanism that uses the HMAC SHA-256 algorithm. @@ -53,15 +43,11 @@ public class SaslMechanismScramSha256 : SaslMechanismScramBase /// /// Creates a new SCRAM-SHA-256 SASL context. /// - /// The URI of the service. /// The user's credentials. - /// Random characters to act as the cnonce token. /// - /// is null. - /// -or- - /// is null. + /// is . /// - internal SaslMechanismScramSha256 (Uri uri, ICredentials credentials, string entropy) : base (uri, credentials, entropy) + public SaslMechanismScramSha256 (NetworkCredential credentials) : base (credentials) { } @@ -71,24 +57,24 @@ internal SaslMechanismScramSha256 (Uri uri, ICredentials credentials, string ent /// /// Creates a new SCRAM-SHA-256 SASL context. /// - /// The URI of the service. - /// The user's credentials. + /// The user name. + /// The password. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// - public SaslMechanismScramSha256 (Uri uri, ICredentials credentials) : base (uri, credentials) + public SaslMechanismScramSha256 (string userName, string password) : base (userName, password) { } /// - /// Gets the name of the mechanism. + /// Get the name of the SASL mechanism. /// /// - /// Gets the name of the mechanism. + /// Gets the name of the SASL mechanism. /// - /// The name of the mechanism. + /// The name of the SASL mechanism. public override string MechanismName { get { return "SCRAM-SHA-256"; } } @@ -119,18 +105,70 @@ protected override KeyedHashAlgorithm CreateHMAC (byte[] key) /// The string. protected override byte[] Hash (byte[] str) { -#if NETFX_CORE - var sha256 = HashAlgorithmProvider.OpenAlgorithm (HashAlgorithmNames.Sha256); - var buf = sha256.HashData (CryptographicBuffer.CreateFromByteArray (str)); - byte[] hash; - - CryptographicBuffer.CopyToByteArray (buf, out hash); - - return hash; -#else using (var sha256 = SHA256.Create ()) return sha256.ComputeHash (str); -#endif + } + } + + /// + /// The SCRAM-SHA-256-PLUS SASL mechanism. + /// + /// + /// A salted challenge/response SASL mechanism that uses the HMAC SHA-256 algorithm and Transport Layer Security (TLS) channel binding. + /// + public class SaslMechanismScramSha256Plus : SaslMechanismScramSha256 + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new SCRAM-SHA-256-PLUS SASL context. + /// + /// The user's credentials. + /// + /// is . + /// + public SaslMechanismScramSha256Plus (NetworkCredential credentials) : base (credentials) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new SCRAM-SHA-256-PLUS SASL context. + /// + /// The user name. + /// The password. + /// + /// is . + /// -or- + /// is . + /// + public SaslMechanismScramSha256Plus (string userName, string password) : base (userName, password) + { + } + + /// + /// Get the name of the SASL mechanism. + /// + /// + /// Gets the name of the SASL mechanism. + /// + /// The name of the SASL mechanism. + public override string MechanismName { + get { return "SCRAM-SHA-256-PLUS"; } + } + + /// + /// Get whether or not the SASL mechanism supports channel binding. + /// + /// + /// Gets whether or not the SASL mechanism supports channel binding. + /// + /// if the SASL mechanism supports channel binding; otherwise, . + public override bool SupportsChannelBinding { + get { return true; } } } } diff --git a/MailKit/Security/SaslMechanismScramSha512.cs b/MailKit/Security/SaslMechanismScramSha512.cs new file mode 100644 index 0000000000..8db046003d --- /dev/null +++ b/MailKit/Security/SaslMechanismScramSha512.cs @@ -0,0 +1,174 @@ +// +// SaslMechanismScramSha512.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Net; +using System.Security.Cryptography; + +namespace MailKit.Security { + /// + /// The SCRAM-SHA-512 SASL mechanism. + /// + /// + /// A salted challenge/response SASL mechanism that uses the HMAC SHA-512 algorithm. + /// + public class SaslMechanismScramSha512 : SaslMechanismScramBase + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new SCRAM-SHA-512 SASL context. + /// + /// The user's credentials. + /// + /// is . + /// + public SaslMechanismScramSha512 (NetworkCredential credentials) : base (credentials) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new SCRAM-SHA-512 SASL context. + /// + /// The user name. + /// The password. + /// + /// is . + /// -or- + /// is . + /// + public SaslMechanismScramSha512 (string userName, string password) : base (userName, password) + { + } + + /// + /// Get the name of the SASL mechanism. + /// + /// + /// Gets the name of the SASL mechanism. + /// + /// The name of the SASL mechanism. + public override string MechanismName { + get { return "SCRAM-SHA-512"; } + } + + /// + /// Create the HMAC context. + /// + /// + /// Creates the HMAC context using the secret key. + /// + /// The HMAC context. + /// The secret key. + protected override KeyedHashAlgorithm CreateHMAC (byte[] key) + { + return new HMACSHA512 (key); + } + + /// + /// Apply the cryptographic hash function. + /// + /// + /// H(str): Apply the cryptographic hash function to the octet string + /// "str", producing an octet string as a result. The size of the + /// result depends on the hash result size for the hash function in + /// use. + /// + /// The results of the hash. + /// The string. + protected override byte[] Hash (byte[] str) + { + using (var sha512 = SHA512.Create ()) + return sha512.ComputeHash (str); + } + } + + /// + /// The SCRAM-SHA-512-PLUS SASL mechanism. + /// + /// + /// A salted challenge/response SASL mechanism that uses the HMAC SHA-512 algorithm and Transport Layer Security (TLS) channel binding. + /// + public class SaslMechanismScramSha512Plus : SaslMechanismScramSha512 + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new SCRAM-SHA-512-PLUS SASL context. + /// + /// The user's credentials. + /// + /// is . + /// + public SaslMechanismScramSha512Plus (NetworkCredential credentials) : base (credentials) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new SCRAM-SHA-512-PLUS SASL context. + /// + /// The user name. + /// The password. + /// + /// is . + /// -or- + /// is . + /// + public SaslMechanismScramSha512Plus (string userName, string password) : base (userName, password) + { + } + + /// + /// Get the name of the SASL mechanism. + /// + /// + /// Gets the name of the SASL mechanism. + /// + /// The name of the SASL mechanism. + public override string MechanismName { + get { return "SCRAM-SHA-512-PLUS"; } + } + + /// + /// Get whether or not the SASL mechanism supports channel binding. + /// + /// + /// Gets whether or not the SASL mechanism supports channel binding. + /// + /// if the SASL mechanism supports channel binding; otherwise, . + public override bool SupportsChannelBinding { + get { return true; } + } + } +} diff --git a/MailKit/Security/SecureSocketOptions.cs b/MailKit/Security/SecureSocketOptions.cs index 7f3da016af..3870e73ab5 100644 --- a/MailKit/Security/SecureSocketOptions.cs +++ b/MailKit/Security/SecureSocketOptions.cs @@ -1,9 +1,9 @@ -// +// // SecureSocketOptions.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -39,8 +39,9 @@ public enum SecureSocketOptions { None, /// - /// Allow the to decide which SSL - /// or TLS options to use (default). + /// Allow the to decide which SSL or TLS + /// options to use (default). If the server does not support SSL or TLS, + /// then the connection will continue without any encryption. /// Auto, diff --git a/MailKit/Security/SslHandshakeException.cs b/MailKit/Security/SslHandshakeException.cs new file mode 100644 index 0000000000..95e98c92fb --- /dev/null +++ b/MailKit/Security/SslHandshakeException.cs @@ -0,0 +1,409 @@ +// +// SslHandshakeException.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Linq; +using System.Text; +using System.Formats.Asn1; +using System.Net.Security; +using System.Globalization; +using System.Collections.Generic; +#if SERIALIZABLE +using System.Security; +using System.Runtime.Serialization; +#endif +using System.Security.Cryptography.X509Certificates; + +namespace MailKit.Security +{ + /// + /// The exception that is thrown when there is an error during the SSL/TLS handshake. + /// + /// + /// The exception that is thrown when there is an error during the SSL/TLS handshake. + /// When this exception occurs, it typically means that the IMAP, POP3 or SMTP server that + /// you are connecting to is using an SSL certificate that is either expired or untrusted by + /// your system. + /// Often times, mail servers will use self-signed certificates instead of using a certificate + /// that has been signed by a trusted Certificate Authority. When your system is unable to validate + /// the mail server's certificate because it is not signed by a known and trusted Certificate Authority, + /// this exception will occur. + /// You can work around this problem by supplying a custom + /// and setting it on the client's property. + /// Most likely, you'll want to compare the thumbprint of the server's certificate with a known + /// value and/or prompt the user to accept the certificate (similar to what you've probably seen web + /// browsers do when they encounter untrusted certificates). + /// +#if SERIALIZABLE + [Serializable] +#endif + public class SslHandshakeException : Exception + { + const string SslHandshakeHelpLink = "https://github.com/jstedfast/MailKit/blob/master/FAQ.md#ssl-handshake-exception"; + const string DefaultMessage = "An error occurred while attempting to establish an SSL or TLS connection."; + +#if SERIALIZABLE + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new from the serialized data. + /// + /// The serialization info. + /// The streaming context. + /// + /// is . + /// + [Obsolete ("This API supports obsolete formatter-based serialization. It should not be called or extended by application code.")] + protected SslHandshakeException (SerializationInfo info, StreamingContext context) : base (info, context) + { + var base64 = info.GetString ("ServerCertificate"); + + if (base64 != null) + ServerCertificate = new X509Certificate2 (Convert.FromBase64String (base64)); + + base64 = info.GetString ("RootCertificateAuthority"); + + if (base64 != null) + RootCertificateAuthority = new X509Certificate2 (Convert.FromBase64String (base64)); + } +#endif + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The error message. + /// An inner exception. + public SslHandshakeException (string message, Exception innerException) : base (message, innerException) + { + HelpLink = SslHandshakeHelpLink; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The error message. + public SslHandshakeException (string message) : base (message) + { + HelpLink = SslHandshakeHelpLink; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + public SslHandshakeException () : base (DefaultMessage) + { + HelpLink = SslHandshakeHelpLink; + } + + /// + /// Get the server's SSL certificate. + /// + /// + /// Gets the server's SSL certificate, if it is available. + /// + /// The server's SSL certificate. + public X509Certificate? ServerCertificate { + get; private set; + } + + /// + /// Get the certificate for the Root Certificate Authority. + /// + /// + /// Gets the certificate for the Root Certificate Authority, if it is available. + /// + /// The Root Certificate Authority certificate. + public X509Certificate? RootCertificateAuthority { + get; private set; + } + +#if SERIALIZABLE + /// + /// When overridden in a derived class, sets the + /// with information about the exception. + /// + /// + /// Sets the + /// with information about the exception. + /// + /// The serialization info. + /// The streaming context. + /// + /// is . + /// + [SecurityCritical] +#if NET8_0_OR_GREATER + [Obsolete ("This API supports obsolete formatter-based serialization. It should not be called or extended by application code.")] +#endif + public override void GetObjectData (SerializationInfo info, StreamingContext context) + { + base.GetObjectData (info, context); + + if (ServerCertificate != null) + info.AddValue ("ServerCertificate", Convert.ToBase64String (ServerCertificate.GetRawCertData ())); + else + info.AddValue ("ServerCertificate", null, typeof (string)); + + if (RootCertificateAuthority != null) + info.AddValue ("RootCertificateAuthority", Convert.ToBase64String (RootCertificateAuthority.GetRawCertData ())); + else + info.AddValue ("RootCertificateAuthority", null, typeof (string)); + } +#endif + + internal static SslHandshakeException Create (ref SslCertificateValidationInfo? validationInfo, Exception ex, bool starttls, string protocol, string host, int port, int sslPort, params int[] standardPorts) + { + var message = new StringBuilder (DefaultMessage); + X509Certificate2? certificate = null; + X509Certificate2? root = null; + + if (ex is AggregateException aggregate) { + aggregate = aggregate.Flatten (); + + if (aggregate.InnerExceptions.Count == 1) + ex = aggregate.InnerExceptions[0]; + else + ex = aggregate; + } + + message.AppendLine (); + message.AppendLine (); + + if (validationInfo != null) { + try { + int rootIndex = validationInfo.ChainElements.Count - 1; + + if (rootIndex > 0) { +#if NET10_0_OR_GREATER + root = X509CertificateLoader.LoadCertificate (validationInfo.ChainElements[rootIndex].Certificate.RawData); +#else + root = new X509Certificate2 (validationInfo.ChainElements[rootIndex].Certificate.RawData); +#endif + } + + if (validationInfo.Certificate != null) { +#if NET10_0_OR_GREATER + certificate = X509CertificateLoader.LoadCertificate (validationInfo.Certificate.RawData); +#else + certificate = new X509Certificate2 (validationInfo.Certificate.RawData); +#endif + } + + if ((validationInfo.SslPolicyErrors & SslPolicyErrors.RemoteCertificateNotAvailable) != 0) { + message.AppendLine ("The SSL certificate for the server was not available."); + } else if ((validationInfo.SslPolicyErrors & SslPolicyErrors.RemoteCertificateNameMismatch) != 0) { + var dnsNames = GetDnsNames (certificate); + + if (dnsNames.Count == 1) { + message.AppendLine ($"The host name ({host}) did not match the name given in the server's SSL certificate ({dnsNames.Single ()})."); + } else { + var formattedDnsNames = string.Join (Environment.NewLine, dnsNames.Select (dnsName => $" \u2022 {dnsName}")); + message.AppendLine ($"The host name ({host}) did not match any of the names given in the server's SSL certificate:{Environment.NewLine}{formattedDnsNames}"); + } + } else { + message.AppendLine ("The server's SSL certificate could not be validated for the following reasons:"); + + bool haveReason = false; + + for (int chainIndex = 0; chainIndex < validationInfo.ChainElements.Count; chainIndex++) { + var element = validationInfo.ChainElements[chainIndex]; + + if (element.ChainElementStatus == null || element.ChainElementStatus.Length == 0) + continue; + + if (chainIndex == 0) { + message.AppendLine ("\u2022 The server certificate has the following errors:"); + } else if (chainIndex == rootIndex) { + message.AppendLine ("\u2022 The root certificate has the following errors:"); + } else { + message.AppendLine ("\u2022 An intermediate certificate has the following errors:"); + } + + foreach (var status in element.ChainElementStatus) { + message.Append (" \u2022 "); + message.AppendLine (status.StatusInformation); + } + + haveReason = true; + } + + // Note: Because Mono does not include any elements in the chain (at least on macOS), we need + // to find the inner-most exception and append its Message. + if (!haveReason) { + var innerException = ex; + + while (innerException.InnerException != null) + innerException = innerException.InnerException; + + message.AppendLine ("\u2022 " + innerException.Message); + } + } + } finally { + validationInfo.Dispose (); + validationInfo = null; + } + } else if (!starttls && standardPorts.Contains (port)) { + string an = "AEHIOS".IndexOf (protocol[0]) != -1 ? "an" : "a"; + + message.AppendFormat (CultureInfo.InvariantCulture, "When connecting to {0} {1} service, port {2} is typically reserved for plain-text connections. If{3}", an, protocol, port, Environment.NewLine); + message.AppendFormat (CultureInfo.InvariantCulture, "you intended to connect to {0} on the SSL port, try connecting to port {1} instead. Otherwise,{2}", protocol, sslPort, Environment.NewLine); + message.AppendLine ("if you intended to use STARTTLS, make sure to use the following code:"); + message.AppendLine (); + message.AppendFormat (CultureInfo.InvariantCulture, "client.Connect (\"{0}\", {1}, SecureSocketOptions.StartTls);{2}", host, port, Environment.NewLine); + } else { + message.AppendLine ("This usually means that the SSL certificate presented by the server is not trusted by the system for one or more of"); + message.AppendLine ("the following reasons:"); + message.AppendLine (); + message.AppendLine ("1. The server is using a self-signed certificate which cannot be verified."); + message.AppendLine ("2. The local system is missing a Root or Intermediate certificate needed to verify the server's certificate."); + message.AppendLine ("3. A Certificate Authority CRL server for one or more of the certificates in the chain is temporarily unavailable."); + message.AppendLine ("4. The certificate presented by the server is expired or invalid."); + message.AppendLine ("5. The set of SSL/TLS protocols supported by the client and server do not match."); + if (!starttls) + message.AppendLine ("6. You are trying to connect to a port which does not support SSL/TLS."); + message.AppendLine (); + message.AppendLine ("See " + SslHandshakeHelpLink + " for possible solutions."); + } + + return new SslHandshakeException (message.ToString (), ex) { ServerCertificate = certificate, RootCertificateAuthority = root }; + } + + // Adapted from Sebastian Krysmanski's https://github.com/skrysmanski/AppMotor/blob/main/src/AppMotor.Core/Certificates/SanExtensionHelpers.cs under the MIT license + static IReadOnlyCollection GetDnsNames (X509Certificate2? certificate) + { + const string subjectAlternativeNameOid = "2.5.29.17"; + var dnsNames = new SortedSet (); + + if (certificate == null) + return dnsNames; + + var dnsNameInfo = certificate.GetNameInfo (X509NameType.DnsName, forIssuer: false); + if (dnsNameInfo != null) + dnsNames.Add (dnsNameInfo); + + var extension = certificate.Extensions[subjectAlternativeNameOid]; + if (extension == null) + return dnsNames; + + try { + // Tag value "2" is defined by: + // + // dNSName [2] IA5String, + // + // in: https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.6 + var dnsNameTag = new Asn1Tag (TagClass.ContextSpecific, tagValue: 2, isConstructed: false); + var asnReader = new AsnReader (extension.RawData, AsnEncodingRules.BER); + var sequenceReader = asnReader.ReadSequence (Asn1Tag.Sequence); + + while (sequenceReader.HasData) { + var tag = sequenceReader.PeekTag (); + if (tag != dnsNameTag) { + sequenceReader.ReadEncodedValue (); + continue; + } + + var dnsName = sequenceReader.ReadCharacterString (UniversalTagNumber.IA5String, dnsNameTag); + dnsNames.Add (dnsName); + } + } catch { + // ignore, the error message will not include subject alternative names + } + + return dnsNames; + } + } + + sealed class SslChainElement : IDisposable + { + public readonly X509Certificate2 Certificate; + public readonly X509ChainStatus[] ChainElementStatus; + public readonly string Information; + + public SslChainElement (X509ChainElement element) + { +#if NET10_0_OR_GREATER + Certificate = X509CertificateLoader.LoadCertificate (element.Certificate.RawData); +#else + Certificate = new X509Certificate2 (element.Certificate.RawData); +#endif + ChainElementStatus = element.ChainElementStatus; + Information = element.Information; + } + + public void Dispose () + { + Certificate.Dispose (); + } + } + + sealed class SslCertificateValidationInfo : IDisposable + { + public readonly List ChainElements; + public readonly X509ChainStatus[] ChainStatus; + public readonly SslPolicyErrors SslPolicyErrors; + public readonly X509Certificate2? Certificate; + public readonly string Host; + + public SslCertificateValidationInfo (string host, X509Certificate? certificate, X509Chain? chain, SslPolicyErrors sslPolicyErrors) + { +#if NET10_0_OR_GREATER + Certificate = certificate != null ? X509CertificateLoader.LoadCertificate (certificate.Export (X509ContentType.Cert)) : null; +#else + Certificate = certificate != null ? new X509Certificate2 (certificate.Export (X509ContentType.Cert)) : null; +#endif + ChainElements = new List (); + SslPolicyErrors = sslPolicyErrors; + Host = host; + + // Note: we need to copy the ChainElements because the chain will be destroyed + if (chain != null) { + ChainStatus = chain.ChainStatus; + + foreach (var element in chain.ChainElements) + ChainElements.Add (new SslChainElement (element)); + } else { + ChainStatus = Array.Empty (); + } + } + + public void Dispose () + { + Certificate?.Dispose (); + foreach (var element in ChainElements) + element.Dispose (); + } + } +} diff --git a/MailKit/ServiceNotAuthenticatedException.cs b/MailKit/ServiceNotAuthenticatedException.cs index e91adb4a4c..304cd285af 100644 --- a/MailKit/ServiceNotAuthenticatedException.cs +++ b/MailKit/ServiceNotAuthenticatedException.cs @@ -1,9 +1,9 @@ -// +// // ServiceNotAuthenticatedException.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -53,9 +53,10 @@ public class ServiceNotAuthenticatedException : InvalidOperationException /// The serialization info. /// The streaming context. /// - /// is null. + /// is . /// [SecuritySafeCritical] + [Obsolete ("This API supports obsolete formatter-based serialization. It should not be called or extended by application code.")] protected ServiceNotAuthenticatedException (SerializationInfo info, StreamingContext context) : base (info, context) { } diff --git a/MailKit/ServiceNotConnectedException.cs b/MailKit/ServiceNotConnectedException.cs index 6d24faa30a..bce97a1f9e 100644 --- a/MailKit/ServiceNotConnectedException.cs +++ b/MailKit/ServiceNotConnectedException.cs @@ -1,9 +1,9 @@ -// +// // ServiceNotConnectedException.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -53,9 +53,10 @@ public class ServiceNotConnectedException : InvalidOperationException /// The serialization info. /// The streaming context. /// - /// is null. + /// is . /// [SecuritySafeCritical] + [Obsolete ("This API supports obsolete formatter-based serialization. It should not be called or extended by application code.")] protected ServiceNotConnectedException (SerializationInfo info, StreamingContext context) : base (info, context) { } diff --git a/MailKit/SpecialFolder.cs b/MailKit/SpecialFolder.cs index a5989eba64..d4167bac54 100644 --- a/MailKit/SpecialFolder.cs +++ b/MailKit/SpecialFolder.cs @@ -1,9 +1,9 @@ -// +// // SpecialFolder.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -48,10 +48,15 @@ public enum SpecialFolder { Drafts, /// - /// The special folder that contains important messages. + /// The special folder that contains flagged messages. /// Flagged, + /// + /// The special folder that contains important messages. + /// + Important, + /// /// The special folder that contains spam messages. /// diff --git a/MailKit/StatusItems.cs b/MailKit/StatusItems.cs index f1f81e75e2..360ee3d38f 100644 --- a/MailKit/StatusItems.cs +++ b/MailKit/StatusItems.cs @@ -1,9 +1,9 @@ -// +// // StatusItems.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -74,5 +74,15 @@ public enum StatusItems { /// Updates . /// AppendLimit = 1 << 6, + + /// + /// Updates . + /// + Size = 1 << 7, + + /// + /// Updates . + /// + MailboxId = 1 << 8, } } diff --git a/MailKit/StoreAction.cs b/MailKit/StoreAction.cs new file mode 100644 index 0000000000..9b70c73fd3 --- /dev/null +++ b/MailKit/StoreAction.cs @@ -0,0 +1,51 @@ +// +// StoreAction.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +namespace MailKit { + /// + /// The action to perform when storing flags, keywords, or labels. + /// + /// + /// Used to specify whether the flags, keywords, or labels should be added, removed, or set for the message(s). + /// + public enum StoreAction + { + /// + /// Add the specified flags, keywords or labels. + /// + Add, + + /// + /// Remove the specified flags, keywords or labels. + /// + Remove, + + /// + /// Replace the specified flags, keywords or labels. + /// + Set + } +} diff --git a/MailKit/StoreFlagsRequest.cs b/MailKit/StoreFlagsRequest.cs new file mode 100644 index 0000000000..eddee78332 --- /dev/null +++ b/MailKit/StoreFlagsRequest.cs @@ -0,0 +1,165 @@ +// +// StoreFlagsRequest.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Collections.Generic; + +namespace MailKit { + /// + /// A request for storing message flags and keywords. + /// + /// + /// A request suitable for storing message flags and keywords. + /// This request is designed to be used with the Store and + /// StoreAsync methods. + /// + public class StoreFlagsRequest : IStoreFlagsRequest + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The store action to perform. + /// The message flags to add, remove or set on the message. + public StoreFlagsRequest (StoreAction action, MessageFlags flags) + { + Keywords = new HashSet (); + Action = action; + Flags = flags; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The store action to perform. + /// The custom keywords to add, remove or set on the message. + /// + /// is . + /// + public StoreFlagsRequest (StoreAction action, IEnumerable keywords) + { + if (keywords == null) + throw new ArgumentNullException (nameof (keywords)); + + Keywords = keywords as ISet ?? new HashSet (keywords); + Action = action; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The store action to perform. + /// The message flags to add, remove or set on the message. + /// The custom keywords to add, remove or set on the message. + /// + /// is . + /// + public StoreFlagsRequest (StoreAction action, MessageFlags flags, IEnumerable keywords) + { + if (keywords == null) + throw new ArgumentNullException (nameof (keywords)); + + Keywords = keywords as ISet ?? new HashSet (keywords); + Action = action; + Flags = flags; + } + + /// + /// Get the store action to perform. + /// + /// + /// Gets the store action to perform. + /// + /// The store action. + public StoreAction Action { + get; private set; + } + + /// + /// Get or set the flags to add, remove or set on the message. + /// + /// + /// Gets or sets the flags to add, remove or set on the message. + /// + /// The message flags. + public MessageFlags Flags { + get; set; + } + + /// + /// Get or set the keywords to add, remove or set on the message. + /// + /// + /// Gets or sets the keywords to add, remove or set on the message. + /// + /// The keywords. + public ISet Keywords { + get; + } + + /// + /// Get or set whether the store operation should run silently. + /// + /// + /// Gets or sets whether the store operation should run silently. + /// Normally, when flags or keywords are changed on a message, a event is emitted. + /// By setting to , this event will not be emitted as a result of this store operation. + /// + /// if the store operation should run silently (not emitting events for flag changes); otherwise, . + public bool Silent { + get; set; + } + + /// + /// Get or set a mod-sequence number that the store operation should use to decide if the flags of a message should be updated or not. + /// + /// + /// Gets or sets a mod-sequence number that the store operation should use to decide if the flags of a message should be updated or not. + /// For each message specified in the message set, the server performs the following. If the mod-sequence of every metadata item of the + /// message affected by the store operation is equal to or less than the specified value, then the requested operation + /// is performed. + /// However, if the mod-sequence of any metadata item of the message is greater than the specified value, then the + /// requested operation WILL NOT be performed. In this case, the mod-sequence attribute of the message is not updated, and the message index + /// (or unique identifier in cases where or + /// is used) is added to the list of + /// messages that failed the UNCHANGEDSINCE test. + /// The mod-sequence number can only be used if the server supports the + /// feature. + /// + /// The mod-sequence number. + public ulong? UnchangedSince { + get; set; + } + } +} diff --git a/MailKit/StoreLabelsRequest.cs b/MailKit/StoreLabelsRequest.cs new file mode 100644 index 0000000000..1d676f9522 --- /dev/null +++ b/MailKit/StoreLabelsRequest.cs @@ -0,0 +1,130 @@ +// +// StoreLabelsRequest.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Collections.Generic; + +namespace MailKit { + /// + /// A request for storing GMail-style labels. + /// + /// + /// A request suitable for storing GMail-style labels. + /// This request is designed to be used with the Store and + /// StoreAsync methods. + /// + public class StoreLabelsRequest : IStoreLabelsRequest + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The store action to perform. + public StoreLabelsRequest (StoreAction action) + { + Labels = new HashSet (); + Action = action; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The store action to perform. + /// The custom keywords to add, remove or set on the message. + /// + /// is . + /// + public StoreLabelsRequest (StoreAction action, IEnumerable labels) + { + if (labels == null) + throw new ArgumentNullException (nameof (labels)); + + Labels = labels as ISet ?? new HashSet (labels); + Action = action; + } + + /// + /// Get the store action to perform. + /// + /// + /// Gets the store action to perform. + /// + /// The store action. + public StoreAction Action { + get; private set; + } + + /// + /// Get the GMail-style labels to add, remove or set on the message. + /// + /// + /// Gets the GMail-style labels to add, remove or set on the message. + /// + /// The GMail-style labels. + public ISet Labels { + get; + } + + /// + /// Get or set whether the store operation should run silently. + /// + /// + /// Gets or sets whether the store operation should run silently. + /// Normally, when flags or keywords are changed on a message, a event is emitted. + /// By setting to , this event will not be emitted as a result of this store operation. + /// + /// if the store operation should run silently (not emitting events for label changes); otherwise, . + public bool Silent { + get; set; + } + + /// + /// Get or set a mod-sequence number that the store operation should use to decide if the labels of a message should be updated or not. + /// + /// + /// Gets or sets a mod-sequence number that the store operation should use to decide if the labels of a message should be updated or not. + /// For each message specified in the message set, the server performs the following. If the mod-sequence of every metadata item of the + /// message affected by the store operation is equal to or less than the specified value, then the requested operation + /// is performed. + /// However, if the mod-sequence of any metadata item of the message is greater than the specified value, then the + /// requested operation WILL NOT be performed. In this case, the mod-sequence attribute of the message is not updated, and the message index + /// (or unique identifier in cases where or + /// is used) is added to the list of + /// messages that failed the UNCHANGEDSINCE test. + /// The mod-sequence number can only be used if the server supports the + /// feature. + /// + /// The mod-sequence number. + public ulong? UnchangedSince { + get; set; + } + } +} diff --git a/MailKit/Telemetry.cs b/MailKit/Telemetry.cs new file mode 100644 index 0000000000..7e1e5bef70 --- /dev/null +++ b/MailKit/Telemetry.cs @@ -0,0 +1,400 @@ +// +// Telemetry.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +#if NET6_0_OR_GREATER + +using System; +using System.Diagnostics; +using System.Diagnostics.Metrics; + +using MailKit.Net; + +namespace MailKit { + /// + /// Telemetry constants for MailKit. + /// + /// + /// Telemetry constants for MailKit. + /// + public static class Telemetry + { + /// + /// The socket-level telemetry information. + /// + /// + /// The socket-level telemetry information. + /// + public static class Socket + { + /// + /// The name of the socket-level meter. + /// + /// + /// The name of the socket-level meter. + /// + public const string MeterName = "mailkit.net.socket"; + + /// + /// The version of the socket-level meter. + /// + /// + /// The version of the socket-level meter. + /// + public const string MeterVersion = "0.1"; + + static Meter? Meter; + + internal static SocketMetrics? Metrics { get; private set; } + + /// + /// Configure socket metering. + /// + /// + /// Configures socket metering. + /// + public static void Configure () + { + Meter ??= new Meter (MeterName, MeterVersion); + Metrics ??= new SocketMetrics (Meter); + } + +#if NET8_0_OR_GREATER + /// + /// Configure socket telemetry. + /// + /// + /// Configures socket telemetry. + /// + /// The meter factory. + /// + /// is . + /// + public static void Configure (IMeterFactory meterFactory) + { + if (meterFactory is null) + throw new ArgumentNullException (nameof (meterFactory)); + + Meter ??= meterFactory.Create (MeterName, MeterVersion); + Metrics ??= new SocketMetrics (Meter); + } +#endif + } + + /// + /// The SmtpClient-level telemetry information. + /// + /// + /// The SmtpClient-level telemetry information. + /// + public static class SmtpClient + { + /// + /// The name of the SmtpClient activity source used for tracing. + /// + /// + /// The name of the SmtpClient activity source used for tracing. + /// + public const string ActivitySourceName = "MailKit.Net.SmtpClient"; + + /// + /// The version of the SmtpClient activity source used for tracing. + /// + /// + /// The version of the SmtpClient activity source used for tracing. + /// + public const string ActivitySourceVersion = "0.1"; + + internal static readonly ActivitySource ActivitySource = new ActivitySource (ActivitySourceName, ActivitySourceVersion); + + /// + /// The name of the SmtpClient meter. + /// + /// + /// The name of the SmtpClient meter. + /// + public const string MeterName = "mailkit.net.smtp"; + + /// + /// The version of the SmtpClient meter. + /// + /// + /// The version of the SmtpClient meter. + /// + public const string MeterVersion = "0.1"; + + static Meter? Meter; + + internal static ClientMetrics? Metrics { get; private set; } + + internal static ClientMetrics CreateMetrics (Meter meter) + { + return new ClientMetrics (meter, MeterName, "an", "SMTP"); + } + + /// + /// Configure SmtpClient telemetry. + /// + /// + /// Configures SmtpClient telemetry. + /// + public static void Configure () + { + Meter ??= new Meter (MeterName, MeterVersion); + Metrics ??= CreateMetrics (Meter); + } + +#if NET8_0_OR_GREATER + /// + /// Configure SmtpClient telemetry. + /// + /// + /// Configures SmtpClient telemetry. + /// + /// The meter factory. + /// + /// is . + /// + public static void Configure (IMeterFactory meterFactory) + { + if (meterFactory is null) + throw new ArgumentNullException (nameof (meterFactory)); + + Meter ??= meterFactory.Create (MeterName, MeterVersion); + Metrics ??= CreateMetrics (Meter); + } +#endif + } + + /// + /// The Pop3Client-level telemetry information. + /// + /// + /// The Pop3Client-level telemetry information. + /// + public static class Pop3Client + { + /// + /// The name of the Pop3Client activity source used for tracing. + /// + /// + /// The name of the Pop3Client activity source used for tracing. + /// + public const string ActivitySourceName = "MailKit.Net.Pop3Client"; + + /// + /// The version of the Pop3Client activity source used for tracing. + /// + /// + /// The version of the Pop3Client activity source used for tracing. + /// + public const string ActivitySourceVersion = "0.1"; + + internal static readonly ActivitySource ActivitySource = new ActivitySource (ActivitySourceName, ActivitySourceVersion); + + /// + /// The name of the Pop3Client meter. + /// + /// + /// The name of the Pop3Client meter. + /// + public const string MeterName = "mailkit.net.pop3"; + + /// + /// The version of the Pop3Client meter. + /// + /// + /// The version of the Pop3Client meter. + /// + public const string MeterVersion = "0.1"; + + static Meter? Meter; + + internal static ClientMetrics? Metrics { get; private set; } + + internal static ClientMetrics CreateMetrics (Meter meter) + { + return new ClientMetrics (meter, MeterName, "a", "POP3"); + } + + /// + /// Configure Pop3Client telemetry. + /// + /// + /// Configures Pop3Client telemetry. + /// + public static void Configure () + { + Meter ??= new Meter (MeterName, MeterVersion); + Metrics ??= CreateMetrics (Meter); + } + +#if NET8_0_OR_GREATER + /// + /// Configure Pop3Client telemetry. + /// + /// + /// Configures Pop3Client telemetry. + /// + /// The meter factory. + /// + /// is . + /// + public static void Configure (IMeterFactory meterFactory) + { + if (meterFactory is null) + throw new ArgumentNullException (nameof (meterFactory)); + + Meter ??= meterFactory.Create (MeterName, MeterVersion); + Metrics ??= CreateMetrics (Meter); + } +#endif + } + + /// + /// The ImapClient-level telemetry information. + /// + /// + /// The ImapClient-level telemetry information. + /// + public static class ImapClient + { + /// + /// The name of the ImapClient activity source used for tracing. + /// + /// + /// The name of the ImapClient activity source used for tracing. + /// + public const string ActivitySourceName = "MailKit.Net.ImapClient"; + + /// + /// The version of the ImapClient activity source used for tracing. + /// + /// + /// The version of the ImapClient activity source used for tracing. + /// + public const string ActivitySourceVersion = "0.1"; + + internal static readonly ActivitySource ActivitySource = new ActivitySource (ActivitySourceName, ActivitySourceVersion); + + /// + /// The name of the ImapClient meter. + /// + /// + /// The name of the ImapClient meter. + /// + public const string MeterName = "mailkit.net.imap"; + + /// + /// The version of the ImapClient meter. + /// + /// + /// The version of the ImapClient meter. + /// + public const string MeterVersion = "0.1"; + + static Meter? Meter; + + internal static ClientMetrics? Metrics { get; private set; } + + internal static ClientMetrics CreateMetrics (Meter meter) + { + return new ClientMetrics (meter, MeterName, "an", "IMAP"); + } + + /// + /// Configure ImapClient telemetry. + /// + /// + /// Configures ImapClient telemetry. + /// + public static void Configure () + { + Meter ??= new Meter (MeterName, MeterVersion); + Metrics ??= CreateMetrics (Meter); + } + +#if NET8_0_OR_GREATER + /// + /// Configure ImapClient telemetry. + /// + /// + /// Configures ImapClient telemetry. + /// + /// The meter factory. + /// + /// is . + /// + public static void Configure (IMeterFactory meterFactory) + { + if (meterFactory is null) + throw new ArgumentNullException (nameof (meterFactory)); + + Meter ??= meterFactory.Create (MeterName, MeterVersion); + Metrics ??= CreateMetrics (Meter); + } +#endif + } + + /// + /// Configure telemetry in MailKit. + /// + /// + /// Configures telemetry in MailKit. + /// + public static void Configure () + { + Socket.Configure (); + SmtpClient.Configure (); + Pop3Client.Configure (); + ImapClient.Configure (); + } + +#if NET8_0_OR_GREATER + /// + /// Configure telemetry in MailKit. + /// + /// + /// Configures telemetry in MailKit. + /// + /// The meter factory. + /// + /// is . + /// + public static void Configure (IMeterFactory meterFactory) + { + if (meterFactory is null) + throw new ArgumentNullException (nameof (meterFactory)); + + Socket.Configure (meterFactory); + SmtpClient.Configure (meterFactory); + Pop3Client.Configure (meterFactory); + ImapClient.Configure (meterFactory); + } +#endif + } +} + +#endif // NET6_0_OR_GREATER diff --git a/MailKit/Security/Ntlm/NtlmSettings.cs b/MailKit/TextEncodings.cs similarity index 61% rename from MailKit/Security/Ntlm/NtlmSettings.cs rename to MailKit/TextEncodings.cs index 8549b9e6dd..373c39dbff 100644 --- a/MailKit/Security/Ntlm/NtlmSettings.cs +++ b/MailKit/TextEncodings.cs @@ -1,10 +1,9 @@ +// +// TextEncodings.cs // -// NtlmSettings.cs +// Author: Jeffrey Stedfast // -// Author: -// Martin Baulig -// -// Copyright (c) 2013-2017 Xamarin Inc. (http://www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -23,23 +22,26 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. +// -namespace MailKit.Security.Ntlm { - /* - * On Windows, this is controlled by a registry setting - * (http://msdn.microsoft.com/en-us/library/ms814176.aspx) - * - * This can be configured by setting the static - * NtlmSettings.DefaultAuthLevel property, the default value - * is LM_and_NTLM_and_try_NTLMv2_Session. - */ - static class NtlmSettings +using System; +using System.Text; + +namespace MailKit { + internal static class TextEncodings { - static NtlmAuthLevel defaultAuthLevel = NtlmAuthLevel.NTLMv2_only; + public static readonly Encoding Latin1; + public static readonly Encoding UTF8; + + static TextEncodings () + { + UTF8 = Encoding.GetEncoding (65001, new EncoderExceptionFallback (), new DecoderExceptionFallback ()); - public static NtlmAuthLevel DefaultAuthLevel { - get { return defaultAuthLevel; } - set { defaultAuthLevel = value; } + try { + Latin1 = Encoding.GetEncoding (28591); + } catch (NotSupportedException) { + Latin1 = Encoding.ASCII; + } } } } diff --git a/MailKit/ThreadingAlgorithm.cs b/MailKit/ThreadingAlgorithm.cs index 043df5ac94..42c4f2e15b 100644 --- a/MailKit/ThreadingAlgorithm.cs +++ b/MailKit/ThreadingAlgorithm.cs @@ -1,9 +1,9 @@ -// +// // ThreadingAlgorithm.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal diff --git a/MailKit/UniqueId.cs b/MailKit/UniqueId.cs index f81b33e117..b1260dc420 100644 --- a/MailKit/UniqueId.cs +++ b/MailKit/UniqueId.cs @@ -1,9 +1,9 @@ -// +// // UniqueId.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -32,9 +32,27 @@ namespace MailKit { /// A unique identifier. /// /// - /// Represents a unique identifier for messages in a . + /// Represents a unique identifier for messages in a . + /// A 32-bit value assigned to each message, which when used with the + /// unique identifier validity value (see below) forms a 64-bit value + /// that MUST NOT refer to any other message in the mailbox or any + /// subsequent mailbox with the same name forever.Unique identifiers + /// are assigned in a strictly ascending fashion in the mailbox; as each + /// message is added to the mailbox it is assigned a higher UID than the + /// message(s) which were added previously. Unlike message sequence + /// numbers, unique identifiers are not necessarily contiguous. + /// The unique identifier of a message MUST NOT change during the + /// session, and SHOULD NOT change between sessions. Any change of + /// unique identifiers between sessions MUST be detectable using the + /// UIDVALIDITY mechanism discussed below. Persistent unique identifiers + /// are required for a client to resynchronize its state from a previous + /// session with the server (e.g., disconnected or offline access + /// clients); this is discussed further in + /// [IMAP-DISC]. + /// For more information about unique identifiers, see + /// RFC 3501, section 2.3.1.1. /// - public struct UniqueId : IComparable, IEquatable + public readonly struct UniqueId : IComparable, IEquatable { /// /// The invalid value. @@ -119,7 +137,7 @@ public uint Id { /// /// Gets the UidValidity of the containing folder. /// - /// The UidValidity of the containing folder. + /// The UidValidity of the containing folder or 0 if not known. public uint Validity { get { return validity; } } @@ -130,7 +148,7 @@ public uint Validity { /// /// Gets whether or not the unique identifier is valid. /// - /// true if the unique identifier is valid; otherwise, false. + /// if the unique identifier is valid; otherwise, . public bool IsValid { get { return Id != 0; } } @@ -141,7 +159,8 @@ public bool IsValid { /// Compares two objects. /// /// - /// Compares two objects. + /// Compares two objects. + /// Validity values are not used in the comparison. /// /// /// A value less than 0 if this is less than , @@ -162,11 +181,12 @@ public int CompareTo (UniqueId other) /// Determines whether the specified is equal to the current . /// /// - /// Determines whether the specified is equal to the current . + /// Determines whether the specified is equal to the current . + /// Validity values are not used in the comparison. /// /// The to compare with the current . - /// true if the specified is equal to the current - /// ; otherwise, false. + /// if the specified is equal to the current + /// ; otherwise, . public bool Equals (UniqueId other) { return other.Id == Id; @@ -178,9 +198,10 @@ public bool Equals (UniqueId other) /// Determines whether two unique identifiers are equal. /// /// - /// Determines whether two unique identifiers are equal. + /// Determines whether two unique identifiers are equal. + /// Validity values are not used in the comparison. /// - /// true if and are equal; otherwise, false. + /// if and are equal; otherwise, . /// The first unique id to compare. /// The second unique id to compare. public static bool operator == (UniqueId uid1, UniqueId uid2) @@ -192,9 +213,10 @@ public bool Equals (UniqueId other) /// Determines whether one unique identifier is greater than another unique identifier. /// /// - /// Determines whether one unique identifier is greater than another unique identifier. + /// Determines whether one unique identifier is greater than another unique identifier. + /// Validity values are not used in the comparison. /// - /// true if is greater than ; otherwise, false. + /// if is greater than ; otherwise, . /// The first unique id to compare. /// The second unique id to compare. public static bool operator > (UniqueId uid1, UniqueId uid2) @@ -206,9 +228,10 @@ public bool Equals (UniqueId other) /// Determines whether one unique identifier is greater than or equal to another unique identifier. /// /// - /// Determines whether one unique identifier is greater than or equal to another unique identifier. + /// Determines whether one unique identifier is greater than or equal to another unique identifier. + /// Validity values are not used in the comparison. /// - /// true if is greater than or equal to ; otherwise, false. + /// if is greater than or equal to ; otherwise, . /// The first unique id to compare. /// The second unique id to compare. public static bool operator >= (UniqueId uid1, UniqueId uid2) @@ -220,9 +243,10 @@ public bool Equals (UniqueId other) /// Determines whether two unique identifiers are not equal. /// /// - /// Determines whether two unique identifiers are not equal. + /// Determines whether two unique identifiers are not equal. + /// Validity values are not used in the comparison. /// - /// true if and are not equal; otherwise, false. + /// if and are not equal; otherwise, . /// The first unique id to compare. /// The second unique id to compare. public static bool operator != (UniqueId uid1, UniqueId uid2) @@ -234,9 +258,10 @@ public bool Equals (UniqueId other) /// Determines whether one unique identifier is less than another unique identifier. /// /// - /// Determines whether one unique identifier is less than another unique identifier. + /// Determines whether one unique identifier is less than another unique identifier. + /// Validity values are not used in the comparison. /// - /// true if is less than ; otherwise, false. + /// if is less than ; otherwise, . /// The first unique id to compare. /// The second unique id to compare. public static bool operator < (UniqueId uid1, UniqueId uid2) @@ -248,9 +273,10 @@ public bool Equals (UniqueId other) /// Determines whether one unique identifier is less than or equal to another unique identifier. /// /// - /// Determines whether one unique identifier is less than or equal to another unique identifier. + /// Determines whether one unique identifier is less than or equal to another unique identifier. + /// Validity values are not used in the comparison. /// - /// true if is less than or equal to ; otherwise, false. + /// if is less than or equal to ; otherwise, . /// The first unique id to compare. /// The second unique id to compare. public static bool operator <= (UniqueId uid1, UniqueId uid2) @@ -262,14 +288,15 @@ public bool Equals (UniqueId other) /// Determines whether the specified is equal to the current . /// /// - /// Determines whether the specified is equal to the current . + /// Determines whether the specified is equal to the current . + /// Validity values are not used in the comparison. /// /// The to compare with the current . - /// true if the specified is equal to the current ; - /// otherwise, false. - public override bool Equals (object obj) + /// if the specified is equal to the current ; + /// otherwise, . + public override bool Equals (object? obj) { - return obj is UniqueId && ((UniqueId) obj).Id == Id; + return obj is UniqueId uid && uid.Id == Id; } /// @@ -302,7 +329,7 @@ public override string ToString () /// /// Attempts to parse a unique identifier. /// - /// true if the unique identifier was successfully parsed; otherwise, false.. + /// if the unique identifier was successfully parsed; otherwise, false.. /// The token to parse. /// The index to start parsing. /// The unique identifier. @@ -339,21 +366,19 @@ internal static bool TryParse (string token, ref int index, out uint uid) /// /// Attempts to parse a unique identifier. /// - /// true if the unique identifier was successfully parsed; otherwise, false.. + /// if the unique identifier was successfully parsed; otherwise, false.. /// The token to parse. /// The UIDVALIDITY value. /// The unique identifier. /// - /// is null. + /// is . /// public static bool TryParse (string token, uint validity, out UniqueId uid) { if (token == null) throw new ArgumentNullException (nameof (token)); - uint id; - - if (!uint.TryParse (token, NumberStyles.None, CultureInfo.InvariantCulture, out id) || id == 0) { + if (!uint.TryParse (token, NumberStyles.None, CultureInfo.InvariantCulture, out uint id) || id == 0) { uid = Invalid; return false; } @@ -369,11 +394,11 @@ public static bool TryParse (string token, uint validity, out UniqueId uid) /// /// Attempts to parse a unique identifier. /// - /// true if the unique identifier was successfully parsed; otherwise, false.. + /// if the unique identifier was successfully parsed; otherwise, false.. /// The token to parse. /// The unique identifier. /// - /// is null. + /// is . /// public static bool TryParse (string token, out UniqueId uid) { @@ -390,7 +415,7 @@ public static bool TryParse (string token, out UniqueId uid) /// A string containing the unique identifier. /// The UIDVALIDITY. /// - /// is null. + /// is . /// /// /// is not in the correct format. @@ -412,7 +437,7 @@ public static UniqueId Parse (string token, uint validity) /// The unique identifier. /// A string containing the unique identifier. /// - /// is null. + /// is . /// /// /// is not in the correct format. diff --git a/MailKit/UniqueIdMap.cs b/MailKit/UniqueIdMap.cs index 0ad7ad0925..4a8d9bf904 100644 --- a/MailKit/UniqueIdMap.cs +++ b/MailKit/UniqueIdMap.cs @@ -1,9 +1,9 @@ -// +// // UniqueIdMap.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -38,12 +38,7 @@ namespace MailKit { /// For example, when copying or moving messages from one folder to another, it is often desirable /// to know what the unique identifiers are for each of the messages in the destination folder. /// - public class UniqueIdMap -#if !NET_4_0 - : IReadOnlyDictionary -#else - : IEnumerable> -#endif + public class UniqueIdMap : IReadOnlyDictionary { /// /// Any empty mapping of unique identifiers. @@ -62,9 +57,9 @@ public class UniqueIdMap /// The unique identifiers used in the source folder. /// The unique identifiers used in the destination folder. /// - /// is null. + /// is . /// -or- - /// is null. + /// is . /// public UniqueIdMap (IList source, IList destination) { @@ -80,7 +75,7 @@ public UniqueIdMap (IList source, IList destination) UniqueIdMap () { - Destination = Source = new UniqueId[0]; + Destination = Source = Array.Empty (); } /// @@ -144,7 +139,7 @@ public IEnumerable Values { /// /// Checks if the specified unique identifier has been remapped. /// - /// true if the unique identifier has been remapped; otherwise, false. + /// if the unique identifier has been remapped; otherwise, . /// The unique identifier. public bool ContainsKey (UniqueId key) { @@ -157,7 +152,7 @@ public bool ContainsKey (UniqueId key) /// /// Attempts to get the remapped unique identifier. /// - /// true on success; otherwise, false. + /// on success; otherwise, . /// The unique identifier of the message in the source folder. /// The unique identifier of the message in the destination folder. public bool TryGetValue (UniqueId key, out UniqueId value) @@ -181,14 +176,13 @@ public bool TryGetValue (UniqueId key, out UniqueId value) /// Gets the remapped unique identifier. /// /// The unique identifier of the message in the source folder. + /// The remapped unique identifier. /// /// is out of range. /// public UniqueId this [UniqueId index] { get { - UniqueId uid; - - if (!TryGetValue (index, out uid)) + if (!TryGetValue (index, out var uid)) throw new ArgumentOutOfRangeException (nameof (index)); return uid; diff --git a/MailKit/UniqueIdRange.cs b/MailKit/UniqueIdRange.cs index 0115d6c0e7..4343dd4364 100644 --- a/MailKit/UniqueIdRange.cs +++ b/MailKit/UniqueIdRange.cs @@ -1,9 +1,9 @@ -// +// // UniqueIdRange.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -29,6 +29,8 @@ using System.Globalization; using System.Collections.Generic; +using MailKit.Search; + namespace MailKit { /// /// A range of items. @@ -41,7 +43,7 @@ namespace MailKit { public class UniqueIdRange : IList { /// - /// A that encompases all messages in the folder. + /// A that encompasses all messages in the folder. /// /// /// Represents the range of messages from to @@ -52,8 +54,8 @@ public class UniqueIdRange : IList static readonly UniqueIdRange Invalid = new UniqueIdRange (); readonly uint validity; - internal uint start; - internal uint end; + readonly uint start; + readonly uint end; /// /// Initializes a new instance of the class. @@ -113,6 +115,17 @@ public UniqueIdRange (UniqueId start, UniqueId end) this.end = end.Id; } + /// + /// Gets the sort order of the unique identifiers. + /// + /// + /// Gets the sort order of the unique identifiers. + /// + /// The sort order. + public SortOrder SortOrder { + get { return start <= end ? SortOrder.Ascending : SortOrder.Descending; } + } + /// /// Gets the validity, if non-zero. /// @@ -178,7 +191,7 @@ public UniqueId End { /// /// The count. public int Count { - get { return (int) (start <= end ? end - start : start - end) + 1; } + get { return (int) Math.Min ((start <= end ? end - start : start - end) + 1, int.MaxValue); } } /// @@ -187,7 +200,7 @@ public int Count { /// /// A is always read-only. /// - /// true if the range is read only; otherwise, false. + /// if the range is read only; otherwise, . public bool IsReadOnly { get { return true; } } @@ -228,7 +241,7 @@ public void Clear () /// /// Determines whether or not the range contains the specified unique id. /// - /// true if the specified unique identifier is in the range; otherwise false. + /// if the specified unique identifier is in the range; otherwise, . /// The unique id. public bool Contains (UniqueId uid) { @@ -248,7 +261,7 @@ public bool Contains (UniqueId uid) /// The array to copy the unique ids to. /// The index into the array. /// - /// is null. + /// is . /// /// /// is out of range. @@ -278,7 +291,7 @@ public void CopyTo (UniqueId[] array, int arrayIndex) /// /// Since a is read-only, unique ids cannot be removed. /// - /// true if the unique identifier was removed; otherwise false. + /// if the unique identifier was removed; otherwise, . /// The unique identifier to remove. /// /// The list does not support removing items. @@ -425,10 +438,7 @@ IEnumerator IEnumerable.GetEnumerator () /// A that represents the current . public override string ToString () { - if (start == end) - return start.ToString (CultureInfo.InvariantCulture); - - if (start <= end && end == uint.MaxValue) + if (end == uint.MaxValue) return string.Format (CultureInfo.InvariantCulture, "{0}:*", start); return string.Format (CultureInfo.InvariantCulture, "{0}:{1}", start, end); @@ -440,12 +450,12 @@ public override string ToString () /// /// Attempts to parse a unique identifier range. /// - /// true if the unique identifier range was successfully parsed; otherwise, false.. + /// if the unique identifier range was successfully parsed; otherwise, false.. /// The token to parse. /// The UIDVALIDITY value. /// The unique identifier range. /// - /// is null. + /// is . /// public static bool TryParse (string token, uint validity, out UniqueIdRange range) { @@ -483,11 +493,11 @@ public static bool TryParse (string token, uint validity, out UniqueIdRange rang /// /// Attempts to parse a unique identifier range. /// - /// true if the unique identifier range was successfully parsed; otherwise, false.. + /// if the unique identifier range was successfully parsed; otherwise, false.. /// The token to parse. /// The unique identifier range. /// - /// is null. + /// is . /// public static bool TryParse (string token, out UniqueIdRange range) { diff --git a/MailKit/UniqueIdSet.cs b/MailKit/UniqueIdSet.cs index ba7848cba1..5254a5a916 100644 --- a/MailKit/UniqueIdSet.cs +++ b/MailKit/UniqueIdSet.cs @@ -1,9 +1,9 @@ -// +// // UniqueIdSet.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -44,8 +44,8 @@ public class UniqueIdSet : IList { struct Range { - public uint Start; - public uint End; + public readonly uint Start; + public readonly uint End; public Range (uint start, uint end) { @@ -154,18 +154,6 @@ public UniqueIdSet (SortOrder order = SortOrder.None) : this (0, order) { } - /// - /// Initializes a new instance of the class. - /// - /// - /// Creates a new unique identifier set. - /// - /// true if unique identifiers should be sorted; otherwise, false. - [Obsolete ("Use UniqueIdSet (SortOrder) instead.")] - public UniqueIdSet (bool sort) : this (sort ? SortOrder.Ascending : SortOrder.None) - { - } - /// /// Initializes a new instance of the class. /// @@ -183,21 +171,6 @@ public UniqueIdSet (IEnumerable uids, SortOrder order = SortOrder.None Add (uid); } - /// - /// Initializes a new instance of the class. - /// - /// - /// Creates a new set of unique identifier set containing the specified uids. - /// - /// An initial set of unique ids. - /// true if unique identifiers should be sorted; otherwise, false. - [Obsolete ("Use UniqueIdSet (IEnumerable, SortOrder) instead.")] - public UniqueIdSet (IEnumerable uids, bool sort) : this (sort) - { - foreach (var uid in uids) - Add (uid); - } - /// /// Gets the sort order of the unique identifiers. /// @@ -239,7 +212,7 @@ public int Count { /// /// Gets whether or not the set is read-only. /// - /// true if the set is read only; otherwise, false. + /// if the set is read only; otherwise, . public bool IsReadOnly { get { return false; } } @@ -510,7 +483,7 @@ public void Clear () /// /// Determines whether or not the set contains the specified unique id. /// - /// true if the specified unique identifier is in the set; otherwise false. + /// if the specified unique identifier is in the set; otherwise, . /// The unique id. public bool Contains (UniqueId uid) { @@ -527,7 +500,7 @@ public bool Contains (UniqueId uid) /// The array to copy the unique ids to. /// The index into the array. /// - /// is null. + /// is . /// /// /// is out of set. @@ -588,7 +561,7 @@ void Remove (int index, uint uid) /// /// Removes the unique identifier from the set. /// - /// true if the unique identifier was removed; otherwise false. + /// if the unique identifier was removed; otherwise, . /// The unique identifier to remove. public bool Remove (UniqueId uid) { @@ -760,47 +733,111 @@ IEnumerator IEnumerable.GetEnumerator () /// A that represents the current . public override string ToString () { + foreach (var subset in EnumerateSerializedSubsets (int.MaxValue)) + return subset; + + return string.Empty; + } + + /// + /// Format a generic list of unique identifiers as a string. + /// + /// + /// Formats a generic list of unique identifiers as a string. + /// + /// The string representation of the collection of unique identifiers. + /// The unique identifiers. + /// + /// is . + /// + /// + /// One or more of the unique identifiers is invalid (has a value of 0). + /// + public static string ToString (IList uids) + { + foreach (var subset in EnumerateSerializedSubsets (uids, int.MaxValue)) + return subset; + + return string.Empty; + } + + /// + /// Format the set of unique identifiers as multiple strings that fit within the maximum defined character length. + /// + /// + /// Formats the set of unique identifiers as multiple strings that fit within the maximum defined character length. + /// + /// A list of strings representing the collection of unique identifiers. + /// The maximum length of any returned string of UIDs. + /// + /// is negative. + /// + IEnumerable EnumerateSerializedSubsets (int maxLength) + { + if (maxLength < 0) + throw new ArgumentOutOfRangeException (nameof (maxLength)); + var builder = new StringBuilder (); for (int i = 0; i < ranges.Count; i++) { - if (i > 0) - builder.Append (','); + var range = ranges[i].ToString (); + + if (builder.Length > 0) { + if (builder.Length + 1 + range.Length > maxLength) { + yield return builder.ToString (); + builder.Clear (); + } else { + builder.Append (','); + } + } - builder.Append (ranges[i]); + builder.Append (range); } - return builder.ToString (); + yield return builder.ToString (); } /// - /// Format a generic list of unique identifiers as a string. + /// Format a generic list of unique identifiers as multiple strings that fit within the maximum defined character length. /// /// - /// Formats a generic list of unique identifiers as a string. + /// Formats a generic list of unique identifiers as multiple strings that fit within the maximum defined character length. /// - /// The string representation of the collection of unique identifiers. + /// A list of strings representing the collection of unique identifiers. /// The unique identifiers. + /// The maximum length of any returned string of UIDs. /// - /// is null. + /// is . /// /// /// One or more of the unique identifiers is invalid (has a value of 0). /// - public static string ToString (IList uids) + /// + /// is negative. + /// + internal static IEnumerable EnumerateSerializedSubsets (IList uids, int maxLength) { if (uids == null) throw new ArgumentNullException (nameof (uids)); - if (uids.Count == 0) - return string.Empty; + if (maxLength < 0) + throw new ArgumentOutOfRangeException (nameof (maxLength)); - var range = uids as UniqueIdRange; - if (range != null) - return range.ToString (); + if (uids.Count == 0) { + yield return string.Empty; + yield break; + } - var set = uids as UniqueIdSet; - if (set != null) - return set.ToString (); + if (uids is UniqueIdRange range) { + yield return range.ToString (); + yield break; + } + + if (uids is UniqueIdSet set) { + foreach (var subset in set.EnumerateSerializedSubsets (maxLength)) + yield return subset; + yield break; + } var builder = new StringBuilder (); int index = 0; @@ -831,18 +868,26 @@ public static string ToString (IList uids) } } - if (builder.Length > 0) - builder.Append (','); - + string next; if (start != end) - builder.AppendFormat ("{0}:{1}", start, end); + next = string.Format (CultureInfo.InvariantCulture, "{0}:{1}", start, end); else - builder.Append (start.ToString ()); + next = start.ToString (); + + if (builder.Length > 0) { + if (builder.Length + 1 + next.Length > maxLength) { + yield return builder.ToString (); + builder.Clear (); + } else { + builder.Append (','); + } + } + builder.Append (next); index = i; } - return builder.ToString (); + yield return builder.ToString (); } /// @@ -851,43 +896,53 @@ public static string ToString (IList uids) /// /// Attempts to parse the specified token as a set of unique identifiers. /// - /// true if the set of unique identifiers were successfully parsed; otherwise, false. + /// if the set of unique identifiers were successfully parsed; otherwise, . /// The token containing the set of unique identifiers. /// The UIDVALIDITY value. /// The set of unique identifiers. + /// The minimum unique identifier value parsed. + /// The maximum unique identifier value parsed. /// - /// is null. + /// is . /// - public static bool TryParse (string token, uint validity, out UniqueIdSet uids) + internal static bool TryParse (string token, uint validity, out UniqueIdSet uids, out UniqueId? minValue, out UniqueId? maxValue) { if (token == null) throw new ArgumentNullException (nameof (token)); uids = new UniqueIdSet (validity); + minValue = maxValue = null; var order = SortOrder.None; + uint min = uint.MaxValue; + uint max = 0; bool sorted = true; - uint start, end; uint prev = 0; int index = 0; do { - if (!UniqueId.TryParse (token, ref index, out start)) + if (!UniqueId.TryParse (token, ref index, out uint start)) return false; + min = Math.Min (min, start); + max = Math.Max (max, start); + if (index < token.Length && token[index] == ':') { index++; - if (!UniqueId.TryParse (token, ref index, out end)) + if (!UniqueId.TryParse (token, ref index, out uint end)) return false; + min = Math.Min (min, end); + max = Math.Max (max, end); + var range = new Range (start, end); uids.count += range.Count; uids.ranges.Add (range); if (sorted) { switch (order) { - default: sorted = true; order = start <= end ? SortOrder.Ascending : SortOrder.Descending; break; + default: order = start <= end ? SortOrder.Ascending : SortOrder.Descending; break; case SortOrder.Descending: sorted = start >= end && start <= prev; break; case SortOrder.Ascending: sorted = start <= end && start >= prev; break; } @@ -900,7 +955,7 @@ public static bool TryParse (string token, uint validity, out UniqueIdSet uids) if (sorted && uids.ranges.Count > 1) { switch (order) { - default: sorted = true; order = start >= prev ? SortOrder.Ascending : SortOrder.Descending; break; + default: order = start >= prev ? SortOrder.Ascending : SortOrder.Descending; break; case SortOrder.Descending: sorted = start <= prev; break; case SortOrder.Ascending: sorted = start >= prev; break; } @@ -918,6 +973,11 @@ public static bool TryParse (string token, uint validity, out UniqueIdSet uids) uids.SortOrder = sorted ? order : SortOrder.None; + if (min <= max) { + minValue = new UniqueId (validity, min); + maxValue = new UniqueId (validity, max); + } + return true; } @@ -927,15 +987,33 @@ public static bool TryParse (string token, uint validity, out UniqueIdSet uids) /// /// Attempts to parse the specified token as a set of unique identifiers. /// - /// true if the set of unique identifiers were successfully parsed; otherwise, false. + /// if the set of unique identifiers were successfully parsed; otherwise, . + /// The token containing the set of unique identifiers. + /// The UIDVALIDITY value. + /// The set of unique identifiers. + /// + /// is . + /// + public static bool TryParse (string token, uint validity, out UniqueIdSet uids) + { + return TryParse (token, validity, out uids, out _, out _); + } + + /// + /// Attempt to parse the specified token as a set of unique identifiers. + /// + /// + /// Attempts to parse the specified token as a set of unique identifiers. + /// + /// if the set of unique identifiers were successfully parsed; otherwise, . /// The token containing the set of unique identifiers. /// The set of unique identifiers. /// - /// is null. + /// is . /// public static bool TryParse (string token, out UniqueIdSet uids) { - return TryParse (token, 0, out uids); + return TryParse (token, 0, out uids, out _, out _); } } } diff --git a/MailKit/UriExtensions.cs b/MailKit/UriExtensions.cs index 952a6531d8..3fec31b994 100644 --- a/MailKit/UriExtensions.cs +++ b/MailKit/UriExtensions.cs @@ -1,9 +1,9 @@ -// +// // UriExtensions.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal diff --git a/MailKit/WebAlertEventArgs.cs b/MailKit/WebAlertEventArgs.cs new file mode 100644 index 0000000000..77db1bbb30 --- /dev/null +++ b/MailKit/WebAlertEventArgs.cs @@ -0,0 +1,73 @@ +// +// WebAlertEventArgs.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MailKit +{ + /// + /// Alert event arguments. + /// + /// + /// Some implementations, such as + /// , will emit WebAlert + /// events when they receive web alert messages from the server. + /// + public class WebAlertEventArgs : AlertEventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The web URI. + /// The alert message. + /// + /// is . + /// -or- + /// is . + /// + public WebAlertEventArgs (Uri uri, string message) : base (message) + { + if (uri == null) + throw new ArgumentNullException (nameof (uri)); + + WebUri = uri; + } + + /// + /// Gets the web URI. + /// + /// + /// The URI that the user should visit to resolve the issue. + /// + /// The web URI. + public Uri WebUri { + get; private set; + } + } +} diff --git a/MailKit/packages.MailKit.Net40.config b/MailKit/packages.MailKit.Net40.config deleted file mode 100644 index 1e2ce4d73e..0000000000 --- a/MailKit/packages.MailKit.Net40.config +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/MailKit/packages.MailKit.Net45.config b/MailKit/packages.MailKit.Net45.config deleted file mode 100644 index cc317314ea..0000000000 --- a/MailKit/packages.MailKit.Net45.config +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/MailKitLite.sln b/MailKitLite.sln new file mode 100644 index 0000000000..da8c4309d0 --- /dev/null +++ b/MailKitLite.sln @@ -0,0 +1,36 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30114.105 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{D5001AA9-4C61-475F-8EA3-4C15949D849F}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MimeKitLite", "submodules\MimeKit\MimeKit\MimeKitLite.csproj", "{23F999AF-CF50-42FF-A011-D56D68E60FB9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MailKitLite", "MailKit\MailKitLite.csproj", "{D6EBFBF3-5806-43A0-B3B3-02EF25C47A9C}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {23F999AF-CF50-42FF-A011-D56D68E60FB9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {23F999AF-CF50-42FF-A011-D56D68E60FB9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {23F999AF-CF50-42FF-A011-D56D68E60FB9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {23F999AF-CF50-42FF-A011-D56D68E60FB9}.Release|Any CPU.Build.0 = Release|Any CPU + {D6EBFBF3-5806-43A0-B3B3-02EF25C47A9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D6EBFBF3-5806-43A0-B3B3-02EF25C47A9C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D6EBFBF3-5806-43A0-B3B3-02EF25C47A9C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D6EBFBF3-5806-43A0-B3B3-02EF25C47A9C}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {80311676-045A-4523-8BD6-AEAD1F21474C} + EndGlobalSection +EndGlobal diff --git a/Makefile b/Makefile deleted file mode 100644 index 063d93c411..0000000000 --- a/Makefile +++ /dev/null @@ -1,29 +0,0 @@ -OUTDIR=MailKit/bin/Release/lib/net40 -ASSEMBLY=$(OUTDIR)/MailKit.dll -XMLDOCS=$(OUTDIR)/MailKit.xml -SOLUTION=MailKit.Net40.sln - -all: - xbuild /target:Build /p:Configuration=Release $(SOLUTION) - -debug: - xbuild /target:Build /p:Configuration=Debug $(SOLUTION) - -clean: - xbuild /target:Clean /p:Configuration=Debug $(SOLUTION) - xbuild /target:Clean /p:Configuration=Release $(SOLUTION) - -check-docs: - @find docs/en -name "*.xml" -exec grep -l "To be added." {} \; - -update-docs: $(ASSEMBLY) - mdoc update --delete -o docs/en $(ASSEMBLY) - -merge-docs: $(ASSEMBLY) $(XMLDOCS) - mdoc update -i $(XMLDOCS) -o docs/en $(ASSEMBLY) - -assemble-docs: - mdoc assemble --out=MailKit docs/en - -html-docs: - mdoc export-html --force-update --template=docs/github-pages.xslt -o ../MailKit-docs/docs docs/en diff --git a/README.md b/README.md index 6653e0832c..526b35bad2 100644 --- a/README.md +++ b/README.md @@ -1,44 +1,52 @@ # MailKit -[![Join the chat at https://gitter.im/jstedfast/MailKit](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/jstedfast/MailKit?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +| Package |Latest Release|Latest Build| +|:----------|:------------:|:----------:| +|**MimeKit**|[![MimeKit NuGet](https://img.shields.io/nuget/v/MimeKit.svg?logo=nuget&style=flat-square)](https://www.nuget.org/packages/MimeKit)![MimeKit NuGet Downloads](https://img.shields.io/nuget/dt/MimeKit.svg?style=flat-square)|[![MimeKit MyGet](https://img.shields.io/myget/mimekit/v/MimeKit.svg?logo=nuget&style=flat-square&label=myget)](https://www.myget.org/feed/mimekit/package/nuget/MimeKit)| +|**MimeKitLite**|[![MimeKitLite NuGet](https://img.shields.io/nuget/v/MimeKitLite.svg?logo=nuget&style=flat-square)](https://www.nuget.org/packages/MimeKitLite)![MimeKitLite NuGet Downloads](https://img.shields.io/nuget/dt/MimeKitLite.svg?style=flat-square)|| +|**MailKit**|[![MailKit NuGet](https://img.shields.io/nuget/v/MailKit.svg?logo=nuget&style=flat-square)](https://www.nuget.org/packages/MailKit)![MailKit NuGet Downloads](https://img.shields.io/nuget/dt/MailKit.svg?style=flat-square)|[![MailKit MyGet](https://img.shields.io/myget/mimekit/v/MailKit.svg?logo=nuget&style=flat-square&label=myget)](https://www.myget.org/feed/mimekit/package/nuget/MailKit)| +|**MailKitLite**|[![MailKitLite NuGet](https://img.shields.io/nuget/v/MailKitLite.svg?logo=nuget&style=flat-square)](https://www.nuget.org/packages/MailKitLite)![MailKitLite NuGet Downloads](https://img.shields.io/nuget/dt/MailKitLite.svg?style=flat-square)|| -[![Issue Stats](http://www.issuestats.com/github/jstedfast/MailKit/badge/pr)](http://www.issuestats.com/github/jstedfast/MailKit) -[![Issue Stats](http://www.issuestats.com/github/jstedfast/MailKit/badge/issue)](http://www.issuestats.com/github/jstedfast/MailKit) -| |Build Status|Code Coverage|Static Analysis| -|-------------|:----------:|:-----------:|:-------------:| -|**Linux/Mac**|[![Build Status](https://travis-ci.org/jstedfast/MailKit.svg)](https://travis-ci.org/jstedfast/MailKit)|[![Code Coverage](https://coveralls.io/repos/jstedfast/MailKit/badge.svg?branch=HEAD)](https://coveralls.io/r/jstedfast/MailKit?branch=HEAD)|[![Static Analysis](https://scan.coverity.com/projects/3202/badge.svg)](https://scan.coverity.com/projects/3202)| -|**Windows** |[![Build Status](https://ci.appveyor.com/api/projects/status/fd38t1ri3cmujnpq/branch/master?svg=true)](https://ci.appveyor.com/project/jstedfast/mailkit/branch/master)|[![Code Coverage](https://coveralls.io/repos/jstedfast/MailKit/badge.svg?branch=HEAD)](https://coveralls.io/r/jstedfast/MailKit?branch=HEAD)|[![Static Analysis](https://scan.coverity.com/projects/3202/badge.svg)](https://scan.coverity.com/projects/3202)| +| Platform |Build Status|Code Coverage|Static Analysis| +|:------------|:----------:|:-----------:|:-------------:| +|**Linux/Mac**|[![Build Status](https://github.com/jstedfast/MailKit/actions/workflows/main.yml/badge.svg?event=push)](https://github.com/jstedfast/MailKit/actions/workflows/main.yml)|[![Code Coverage](https://img.shields.io/coverallsCoverage/github/jstedfast/MailKit?branch=master)](https://coveralls.io/r/jstedfast/MailKit?branch=master)|[![Static Analysis](https://img.shields.io/coverity/scan/3202)](https://scan.coverity.com/projects/3202)| +|**Windows** |[![Build Status](https://github.com/jstedfast/MailKit/actions/workflows/main.yml/badge.svg?event=push)](https://github.com/jstedfast/MailKit/actions/workflows/main.yml)|[![Code Coverage](https://img.shields.io/coverallsCoverage/github/jstedfast/MailKit?branch=master)](https://coveralls.io/r/jstedfast/MailKit?branch=master)|[![Static Analysis](https://img.shields.io/coverity/scan/3202)](https://scan.coverity.com/projects/3202)| ## What is MailKit? MailKit is a cross-platform mail client library built on top of [MimeKit](https://github.com/jstedfast/MimeKit). +## Donate + +MailKit is a personal open source project that I have put thousands of hours into perfecting with the +goal of making it the very best email framework for .NET. I need your help to achieve this. + +Donating helps pay for things such as web hosting, domain registration and licenses for developer tools +such as a performance profiler, memory profiler, a static code analysis tool, and more. It also helps +motivate me to continue working on the project. + +Click here to lend your support to MailKit by making a donation! + ## Features * SASL Authentication - * CRAM-MD5 - * DIGEST-MD5 - * LOGIN - * NTLM - * PLAIN - * SCRAM-SHA-1 - * SCRAM-SHA-256 - * XOAUTH2 (partial support - you need to fetch the auth tokens yourself) + * Supports the following mechanisms: [CRAM-MD5](https://tools.ietf.org/html/rfc2195), [DIGEST-MD5](https://tools.ietf.org/html/rfc2831), + [LOGIN](https://tools.ietf.org/html/draft-murchison-sasl-login-00), [NTLM](https://davenport.sourceforge.net/ntlm.html), + [PLAIN](https://tools.ietf.org/html/rfc2595), [SCRAM-SHA-1[-PLUS]](https://tools.ietf.org/html/rfc5802), + [SCRAM-SHA-256[-PLUS]](https://tools.ietf.org/html/rfc5802), [SCRAM-SHA-512[-PLUS]](https://tools.ietf.org/html/draft-melnikov-scram-sha-512-04), + [OAUTHBEARER](https://tools.ietf.org/html/rfc7628) and XOAUTH2 +* Proxy Support + * Supports the following types of proxies: [SOCKS4/4a](https://www.openssh.com/txt/socks4.protocol), [SOCKS5](https://tools.ietf.org/html/rfc1928), + and [HTTP/S](https://tools.ietf.org/html/rfc2616) * SMTP Client * Supports all of the SASL mechanisms listed above. * Supports SSL-wrapped connections via the "smtps" protocol. * Supports client SSL/TLS certificates. - * Supports the following extensions: - * [SIZE](https://tools.ietf.org/html/rfc1870) - * [DSN](https://tools.ietf.org/html/rfc1891) - * [AUTH](https://tools.ietf.org/html/rfc2554) - * [8BITMIME](https://tools.ietf.org/html/rfc2821) - * [PIPELINING](https://tools.ietf.org/html/rfc2920) - * [BINARYMIME](https://tools.ietf.org/html/rfc3030) - * [CHUNKING](https://tools.ietf.org/html/rfc3030) - * [STARTTLS](https://tools.ietf.org/html/rfc3207) - * [SMTPUTF8](https://tools.ietf.org/html/rfc6531) + * Supports the following extensions: [SIZE](https://tools.ietf.org/html/rfc1870), [DSN](https://tools.ietf.org/html/rfc1891), + [AUTH](https://tools.ietf.org/html/rfc2554), [8BITMIME](https://tools.ietf.org/html/rfc2821), [PIPELINING](https://tools.ietf.org/html/rfc2920), + [BINARYMIME](https://tools.ietf.org/html/rfc3030), [CHUNKING](https://tools.ietf.org/html/rfc3030), [STARTTLS](https://tools.ietf.org/html/rfc3207), + and [SMTPUTF8](https://tools.ietf.org/html/rfc6531) * All APIs are cancellable. * Async APIs are available. * POP3 Client @@ -46,61 +54,31 @@ MailKit is a cross-platform mail client library built on top of [MimeKit](https: * Also supports authentication via [APOP](https://tools.ietf.org/html/rfc1939#page-15) and `USER`/`PASS`. * Supports SSL-wrapped connections via the "pops" protocol. * Supports client SSL/TLS certificates. - * Supports the following extensions: - * [TOP](https://tools.ietf.org/html/rfc1939#page-11) - * [UIDL](https://tools.ietf.org/html/rfc1939#page-12) - * [EXPIRE](https://tools.ietf.org/html/rfc2449) - * [LOGIN-DELAY](https://tools.ietf.org/html/rfc2449) - * [PIPELINING](https://tools.ietf.org/html/rfc2449) - * [SASL](https://tools.ietf.org/html/rfc2449) - * [STLS](https://tools.ietf.org/html/rfc2595) - * [UTF8](https://tools.ietf.org/html/rfc6856) - * [UTF8=USER](https://tools.ietf.org/html/rfc6856) - * [LANG](https://tools.ietf.org/html/rfc6856) + * Supports the following extensions: [TOP](https://tools.ietf.org/html/rfc1939#page-11), [UIDL](https://tools.ietf.org/html/rfc1939#page-12), + [EXPIRE](https://tools.ietf.org/html/rfc2449), [LOGIN-DELAY](https://tools.ietf.org/html/rfc2449), [PIPELINING](https://tools.ietf.org/html/rfc2449), + [SASL](https://tools.ietf.org/html/rfc2449), [STLS](https://tools.ietf.org/html/rfc2595), [UTF8](https://tools.ietf.org/html/rfc6856), + [UTF8=USER](https://tools.ietf.org/html/rfc6856), and [LANG](https://tools.ietf.org/html/rfc6856) * All APIs are cancellable. * Async APIs are available. * IMAP4 Client * Supports all of the SASL mechanisms listed above. * Supports SSL-wrapped connections via the "imaps" protocol. * Supports client SSL/TLS certificates. - * Supports the following extensions: - * [ACL](https://tools.ietf.org/html/rfc4314) - * [QUOTA](https://tools.ietf.org/html/rfc2087) - * [LITERAL+](https://tools.ietf.org/html/rfc2088) - * [IDLE](https://tools.ietf.org/html/rfc2177) - * [NAMESPACE](https://tools.ietf.org/html/rfc2342) - * [ID](https://tools.ietf.org/html/rfc2971) - * [CHILDREN](https://tools.ietf.org/html/rfc3348) - * [LOGINDISABLED](https://tools.ietf.org/html/rfc3501) - * [STARTTLS](https://tools.ietf.org/html/rfc3501) - * [MULTIAPPEND](https://tools.ietf.org/html/rfc3502) - * [UNSELECT](https://tools.ietf.org/html/rfc3691) - * [UIDPLUS](https://tools.ietf.org/html/rfc4315) - * [CONDSTORE](https://tools.ietf.org/html/rfc4551) - * [ESEARCH](https://tools.ietf.org/html/rfc4731) - * [SASL-IR](https://tools.ietf.org/html/rfc4959) - * [COMPRESS](https://tools.ietf.org/html/rfc4978) - * [WITHIN](https://tools.ietf.org/html/rfc5032) - * [ENABLE](https://tools.ietf.org/html/rfc5161) - * [QRESYNC](https://tools.ietf.org/html/rfc5162) - * [SORT](https://tools.ietf.org/html/rfc5256) - * [THREAD](https://tools.ietf.org/html/rfc5256) - * [LIST-EXTENDED](https://tools.ietf.org/html/rfc5258) - * [ESORT](https://tools.ietf.org/html/rfc5267) - * [METADATA](https://tools.ietf.org/html/rfc5464) - * [FILTERS](https://tools.ietf.org/html/rfc5466) - * [LIST-STATUS](https://tools.ietf.org/html/rfc5819) - * [SORT=DISPLAY](https://tools.ietf.org/html/rfc5957) - * [SPECIAL-USE](https://tools.ietf.org/html/rfc6154) - * [CREATE-SPECIAL-USE](https://tools.ietf.org/html/rfc6154) - * [SEARCH=FUZZY](https://tools.ietf.org/html/rfc6203) - * [MOVE](https://tools.ietf.org/html/rfc6851) - * [UTF8=ACCEPT](https://tools.ietf.org/html/rfc6855) - * [UTF8=ONLY](https://tools.ietf.org/html/rfc6855) - * [LITERAL-](https://tools.ietf.org/html/rfc7888) - * [APPENDLIMIT](https://tools.ietf.org/html/rfc7889) - * [XLIST](https://developers.google.com/gmail/imap_extensions) - * [X-GM-EXT1](https://developers.google.com/gmail/imap_extensions) (X-GM-MSGID, X-GM-THRID, X-GM-RAW and X-GM-LABELS) + * Supports the following extensions: [ACL](https://tools.ietf.org/html/rfc4314), [QUOTA](https://tools.ietf.org/html/rfc2087), + [LITERAL+](https://tools.ietf.org/html/rfc2088), [IDLE](https://tools.ietf.org/html/rfc2177), [NAMESPACE](https://tools.ietf.org/html/rfc2342), + [ID](https://tools.ietf.org/html/rfc2971), [CHILDREN](https://tools.ietf.org/html/rfc3348), [LOGINDISABLED](https://tools.ietf.org/html/rfc3501), + [STARTTLS](https://tools.ietf.org/html/rfc3501), [MULTIAPPEND](https://tools.ietf.org/html/rfc3502), [UNSELECT](https://tools.ietf.org/html/rfc3691), + [UIDPLUS](https://tools.ietf.org/html/rfc4315), [CONDSTORE](https://tools.ietf.org/html/rfc4551), [ESEARCH](https://tools.ietf.org/html/rfc4731), + [SASL-IR](https://tools.ietf.org/html/rfc4959), [COMPRESS](https://tools.ietf.org/html/rfc4978), [WITHIN](https://tools.ietf.org/html/rfc5032), + [ENABLE](https://tools.ietf.org/html/rfc5161), [QRESYNC](https://tools.ietf.org/html/rfc5162), [SORT](https://tools.ietf.org/html/rfc5256), + [THREAD](https://tools.ietf.org/html/rfc5256), [ANNOTATE](https://tools.ietf.org/html/rfc5257), [LIST-EXTENDED](https://tools.ietf.org/html/rfc5258), + [ESORT](https://tools.ietf.org/html/rfc5267), [METADATA / METADATA-SERVER](https://tools.ietf.org/html/rfc5464), [NOTIFY](https://tools.ietf.org/html/rfc5465), + [FILTERS](https://tools.ietf.org/html/rfc5466), [LIST-STATUS](https://tools.ietf.org/html/rfc5819), [SORT=DISPLAY](https://tools.ietf.org/html/rfc5957), + [SPECIAL-USE / CREATE-SPECIAL-USE](https://tools.ietf.org/html/rfc6154), [SEARCH=FUZZY](https://tools.ietf.org/html/rfc6203), + [MOVE](https://tools.ietf.org/html/rfc6851), [UTF8=ACCEPT / UTF8=ONLY](https://tools.ietf.org/html/rfc6855), [LITERAL-](https://tools.ietf.org/html/rfc7888), + [APPENDLIMIT](https://tools.ietf.org/html/rfc7889), [STATUS=SIZE](https://tools.ietf.org/html/rfc8438), [OBJECTID](https://tools.ietf.org/html/rfc8474), + [REPLACE](https://tools.ietf.org/html/rfc8508), [SAVEDATE](https://tools.ietf.org/html/rfc8514), [XLIST](https://developers.google.com/gmail/imap_extensions), + and [X-GM-EXT1](https://developers.google.com/gmail/imap_extensions) (X-GM-MSGID, X-GM-THRID, X-GM-RAW and X-GM-LABELS) * All APIs are cancellable. * Async APIs are available. * Client-side sorting and threading of messages. @@ -141,32 +119,36 @@ which should improve performance of sending messages (although might not be very ## License Information -MailKit is Copyright (C) 2013-2017 Xamarin Inc. and is licensed under the MIT license: +```text +MIT License + +Copyright (C) 2013-2026 .NET Foundation and Contributors - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +``` ## Installing via NuGet The easiest way to install MailKit is via [NuGet](https://www.nuget.org/packages/MailKit/). -In Visual Studio's [Package Manager Console](http://docs.nuget.org/docs/start-here/using-the-package-manager-console), -simply enter the following command: +In Visual Studio's [Package Manager Console](https://docs.nuget.org/docs/start-here/using-the-package-manager-console), +enter the following command: Install-Package MailKit @@ -188,7 +170,7 @@ Fill in the areas outlined in red and then click **OK**. This will recursively c ## Updating the Source Code Occasionally you might want to update your local copy of the source code if I have made changes to MailKit since you -downloaded the source code in the step above. To do this using the command-line version fo Git, you'll need to issue +downloaded the source code in the step above. To do this using the command-line version of Git, you'll need to issue the following commands in your terminal within the MailKit directory: git pull @@ -204,17 +186,18 @@ directory and select **Git Sync...** in the menu. Once you do that, you'll need In the top-level MailKit directory, there are a number of solution files; they are: -* **MailKit.sln** - includes the projects for .NET 4.0, .NET 4.5, .NETStandard 1.3, Windows Universal 8.1, - Xamarin.Android, and Xamarin.iOS. -* **MailKit.Mobile.sln** - includes only the Xamarin.iOS and Xamarin.Android projects. -* **MailKit.Net45.sln** - includes only the .NET 4.5 project and the unit tests. -* **MailKit.Net40.sln** - includes only the .NET 4.0 project. +* **MailKit.sln** - includes the projects for .NET Framework 4.6.2/4.7/4.8, .NETStandard 2.0/2.1, .NET6.0 as well as the unit tests. +* **MailKit.Coverity.sln** - this is used to generate Coverity static analysis builds and is not generally useful. +* **MailKit.Documentation.sln** - this is used to generate the documentation found at https://mimekit.net/docs -If you don't have the Xamarin products, you'll probably want to open the MailKit.Net45.sln instead of MailKit.sln. +Once you've opened the appropriate MailKit solution file in [Visual Studio](https://www.visualstudio.com/downloads/), +you can choose the **Debug** or **Release** build configuration and then build. -Once you've opened the appropriate MailKit solution file in either [Xamarin Studio](https://www.xamarin.com/download) -or [Visual Studio 2017](https://www.visualstudio.com/downloads/), you can simply choose the **Debug** or **Release** -build configuration and then build. +Both Visual Studio 2017 and Visual Studio 2019 should be able to build MailKit without any issues, but older versions such as +Visual Studio 2015 will require modifications to the projects in order to build correctly. It has been reported that adding +NuGet package references to [Microsoft.Net.Compilers](https://www.nuget.org/packages/Microsoft.Net.Compilers/) >= 3.6.0 +and [System.ValueTuple](https://www.nuget.org/packages/System.ValueTuple/) >= 4.5.0 to the MimeKit and MailKit projects will +allow them to build successfully. Note: The **Release** build will generate the xml API documentation, but the **Debug** build will not. @@ -232,41 +215,34 @@ using MailKit; using MimeKit; namespace TestClient { - class Program - { - public static void Main (string[] args) - { - var message = new MimeMessage (); - message.From.Add (new MailboxAddress ("Joey Tribbiani", "joey@friends.com")); - message.To.Add (new MailboxAddress ("Mrs. Chanandler Bong", "chandler@friends.com")); - message.Subject = "How you doin'?"; - - message.Body = new TextPart ("plain") { - Text = @"Hey Chandler, + class Program + { + public static void Main (string[] args) + { + var message = new MimeMessage (); + message.From.Add (new MailboxAddress ("Joey Tribbiani", "joey@friends.com")); + message.To.Add (new MailboxAddress ("Mrs. Chanandler Bong", "chandler@friends.com")); + message.Subject = "How you doin'?"; + + message.Body = new TextPart ("plain") { + Text = @"Hey Chandler, I just wanted to let you know that Monica and I were going to go play some paintball, you in? -- Joey" - }; - - using (var client = new SmtpClient ()) { - // For demo-purposes, accept all SSL certificates (in case the server supports STARTTLS) - client.ServerCertificateValidationCallback = (s,c,h,e) => true; + }; - client.Connect ("smtp.friends.com", 587, false); + using (var client = new SmtpClient ()) { + client.Connect ("smtp.friends.com", 587, false); - // Note: since we don't have an OAuth2 token, disable - // the XOAUTH2 authentication mechanism. - client.AuthenticationMechanisms.Remove ("XOAUTH2"); + // Note: only needed if the SMTP server requires authentication + client.Authenticate ("joey", "password"); - // Note: only needed if the SMTP server requires authentication - client.Authenticate ("joey", "password"); - - client.Send (message); - client.Disconnect (true); - } - } - } + client.Send (message); + client.Disconnect (true); + } + } + } } ``` @@ -282,111 +258,117 @@ using MailKit; using MimeKit; namespace TestClient { - class Program - { - public static void Main (string[] args) - { - using (var client = new Pop3Client ()) { - // For demo-purposes, accept all SSL certificates (in case the server supports STARTTLS) - client.ServerCertificateValidationCallback = (s,c,h,e) => true; - - client.Connect ("pop.friends.com", 110, false); - - // Note: since we don't have an OAuth2 token, disable - // the XOAUTH2 authentication mechanism. - client.AuthenticationMechanisms.Remove ("XOAUTH2"); - - client.Authenticate ("joey", "password"); - - for (int i = 0; i < client.Count; i++) { - var message = client.GetMessage (i); - Console.WriteLine ("Subject: {0}", message.Subject); - } - - client.Disconnect (true); - } - } - } + class Program + { + public static void Main (string[] args) + { + using (var client = new Pop3Client ()) { + client.Connect ("pop.friends.com", 110, false); + + client.Authenticate ("joey", "password"); + + for (int i = 0; i < client.Count; i++) { + var message = client.GetMessage (i); + Console.WriteLine ("Subject: {0}", message.Subject); + } + + client.Disconnect (true); + } + } + } } ``` ## Using IMAP -More important than POP3 support is the IMAP support. Here's a simple use-case of retreiving messages from an IMAP server: +More important than POP3 support is the IMAP support. Here's a simple use-case of retrieving messages from an IMAP server: ```csharp using System; -using MailKit.Net.Imap; -using MailKit.Search; -using MailKit; using MimeKit; +using MailKit; +using MailKit.Search; +using MailKit.Net.Imap; namespace TestClient { - class Program - { - public static void Main (string[] args) - { - using (var client = new ImapClient ()) { - // For demo-purposes, accept all SSL certificates - client.ServerCertificateValidationCallback = (s,c,h,e) => true; - - client.Connect ("imap.friends.com", 993, true); - - // Note: since we don't have an OAuth2 token, disable - // the XOAUTH2 authentication mechanism. - client.AuthenticationMechanisms.Remove ("XOAUTH2"); - - client.Authenticate ("joey", "password"); - - // The Inbox folder is always available on all IMAP servers... - var inbox = client.Inbox; - inbox.Open (FolderAccess.ReadOnly); - - Console.WriteLine ("Total messages: {0}", inbox.Count); - Console.WriteLine ("Recent messages: {0}", inbox.Recent); - - for (int i = 0; i < inbox.Count; i++) { - var message = inbox.GetMessage (i); - Console.WriteLine ("Subject: {0}", message.Subject); - } - - client.Disconnect (true); - } - } - } + class Program + { + public static void Main (string[] args) + { + using (var client = new ImapClient ()) { + client.Connect ("imap.friends.com", 993, true); + + client.Authenticate ("joey", "password"); + + // The Inbox folder is always available on all IMAP servers... + var inbox = client.Inbox; + inbox.Open (FolderAccess.ReadOnly); + + Console.WriteLine ("Total messages: {0}", inbox.Count); + Console.WriteLine ("Recent messages: {0}", inbox.Recent); + + for (int i = 0; i < inbox.Count; i++) { + var message = inbox.GetMessage (i); + Console.WriteLine ("Subject: {0}", message.Subject); + } + + client.Disconnect (true); + } + } + } } ``` -However, you probably want to do more complicated things with IMAP such as fetching summary information -so that you can display a list of messages in a mail client without having to first download all of the -messages from the server: +### Fetching Information About the Messages in an IMAP Folder + +One of the advantages of IMAP over POP3 is that the IMAP protocol allows clients to retrieve information about +the messages in a folder without having to first download all of them. + +Using the [Fetch](https://www.mimekit.net/docs/html/Overload_MailKit_Net_Imap_ImapFolder_Fetch.htm) and +[FetchAsync](https://www.mimekit.net/docs/html/Overload_MailKit_Net_Imap_ImapFolder_FetchAsync.htm) method overloads +(or the convenient [extension methods](https://www.mimekit.net/docs/html/Overload_MailKit_IMailFolderExtensions_Fetch.htm)), +it's possible to obtain any subset of summary information for any range of messages in a given folder. ```csharp -foreach (var summary in inbox.Fetch (0, -1, MessageSummaryItems.Full | MessageSummaryItems.UniqueId)) { - Console.WriteLine ("[summary] {0:D2}: {1}", summary.Index, summary.Envelope.Subject); -} +foreach (var summary in inbox.Fetch (0, -1, MessageSummaryItems.Envelope)) { + Console.WriteLine ("[summary] {0:D2}: {1}", summary.Index, summary.Envelope.Subject); ``` -The results of a Fetch command can also be used to download individual MIME parts rather +It's also possible to use Fetch/FetchAsync APIs that take an [IFetchRequest](https://www.mimekit.net/docs/html/T_MailKit_IFetchRequest.htm) +argument to get even more control over what to fetch: + +```csharp +// Let's Fetch non-Received headers: +var request = new FetchRequest { + Headers = new HeaderSet (new HeaderId[] { HeaderId.Received }) { + Exclude = true + } +}; + +foreach (var summary in inbox.Fetch (0, -1, request)) { + Console.WriteLine ("[summary] {0:D2}: {1}", summary.Index, summary.Headers[HeaderId.Subject]); +``` + +The results of a Fetch method can also be used to download individual MIME parts rather than downloading the entire message. For example: ```csharp foreach (var summary in inbox.Fetch (0, -1, MessageSummaryItems.UniqueId | MessageSummaryItems.BodyStructure)) { if (summary.TextBody != null) { - // this will download *just* the text/plain part - var text = inbox.GetBodyPart (summary.UniqueId, summary.TextBody); + // this will download *just* the text/plain part + var text = inbox.GetBodyPart (summary.UniqueId, summary.TextBody); } - + if (summary.HtmlBody != null) { // this will download *just* the text/html part - var html = inbox.GetBodyPart (summary.UniqueId, summary.HtmlBody); + var html = inbox.GetBodyPart (summary.UniqueId, summary.HtmlBody); } - + // if you'd rather grab, say, an image attachment... it might look something like this: if (summary.Body is BodyPartMultipart) { var multipart = (BodyPartMultipart) summary.Body; - + var attachment = multipart.BodyParts.OfType ().FirstOrDefault (x => x.FileName == "logo.jpg"); if (attachment != null) { // this will download *just* the attachment @@ -396,6 +378,31 @@ foreach (var summary in inbox.Fetch (0, -1, MessageSummaryItems.UniqueId | Messa } ``` +### Setting Message Flags in IMAP + +In order to set or update the flags on a particular message, what is actually needed is the UID or index of the message and +the folder that it belongs to. + +An obvious reason to want to update message flags is to mark a message as "read" (aka "seen") after a user has opened a +message and read it. + +```csharp +folder.Store (uid, new StoreFlagsRequest (StoreAction.Add, MessageFlags.Seen) { Silent = true }); +``` + +### Deleting Messages in IMAP + +Deleting messages in IMAP involves setting a `\Deleted` flag on a message and, optionally, expunging it from the folder. + +The way to mark a message as `\Deleted` works the same way as marking a message as `\Seen`. + +```csharp +folder.Store (uid, new StoreFlagsRequest (StoreAction.Add, MessageFlags.Deleted) { Silent = true }); +folder.Expunge (); +``` + +### Searching an IMAP Folder + You may also be interested in sorting and searching... ```csharp @@ -404,46 +411,48 @@ var query = SearchQuery.DeliveredAfter (DateTime.Parse ("2013-01-12")) .And (SearchQuery.SubjectContains ("MailKit")).And (SearchQuery.Seen); foreach (var uid in inbox.Search (query)) { - var message = inbox.GetMessage (uid); - Console.WriteLine ("[match] {0}: {1}", uid, message.Subject); + var message = inbox.GetMessage (uid); + Console.WriteLine ("[match] {0}: {1}", uid, message.Subject); } // let's do the same search, but this time sort them in reverse arrival order var orderBy = new [] { OrderBy.ReverseArrival }; -foreach (var uid in inbox.Search (query, orderBy)) { - var message = inbox.GetMessage (uid); - Console.WriteLine ("[match] {0}: {1}", uid, message.Subject); +foreach (var uid in inbox.Sort (query, orderBy)) { + var message = inbox.GetMessage (uid); + Console.WriteLine ("[match] {0}: {1}", uid, message.Subject); } // you'll notice that the orderBy argument is an array... this is because you // can actually sort the search results based on multiple columns: orderBy = new [] { OrderBy.ReverseArrival, OrderBy.Subject }; -foreach (var uid in inbox.Search (query, orderBy)) { - var message = inbox.GetMessage (uid); - Console.WriteLine ("[match] {0}: {1}", uid, message.Subject); +foreach (var uid in inbox.Sort (query, orderBy)) { + var message = inbox.GetMessage (uid); + Console.WriteLine ("[match] {0}: {1}", uid, message.Subject); } ``` Of course, instead of downloading the message, you could also fetch the summary information for the matching messages or do any of a number of other things with the UIDs that are returned. +### Navigating Folders in IMAP + How about navigating folders? MailKit can do that, too: ```csharp // Get the first personal namespace and list the toplevel folders under it. var personal = client.GetFolder (client.PersonalNamespaces[0]); foreach (var folder in personal.GetSubfolders (false)) - Console.WriteLine ("[folder] {0}", folder.Name); + Console.WriteLine ("[folder] {0}", folder.Name); ``` -If the IMAP server supports the SPECIAL-USE or the XLIST (GMail) extension, you can get ahold of +If the IMAP server supports the SPECIAL-USE or the XLIST (GMail) extension, you can get a hold of the pre-defined All, Drafts, Flagged (aka Important), Junk, Sent, Trash, etc folders like this: ```csharp if ((client.Capabilities & (ImapCapabilities.SpecialUse | ImapCapabilities.XList)) != 0) { - var drafts = client.GetFolder (SpecialFolder.Drafts); + var drafts = client.GetFolder (SpecialFolder.Drafts); } else { - // maybe check the user's preferences for the Drafts folder? + // maybe check the user's preferences for the Drafts folder? } ``` @@ -452,7 +461,7 @@ come up with your own heuristics for getting the Sent, Drafts, Trash, etc folder might use logic similar to this: ```csharp -static string[] CommonSentFolderNames = { "Sent Items", "Sent Mail", /* maybe add some translated names */ }; +static string[] CommonSentFolderNames = { "Sent Items", "Sent Mail", "Sent Messages", /* maybe add some translated names */ }; static IFolder GetSentFolder (ImapClient client, CancellationToken cancellationToken) { @@ -460,7 +469,7 @@ static IFolder GetSentFolder (ImapClient client, CancellationToken cancellationT foreach (var folder in personal.GetSubfolders (false, cancellationToken)) { foreach (var name in CommonSentFolderNames) { - if (folder.Name == commonName) + if (folder.Name == name) return folder; } } @@ -472,17 +481,18 @@ static IFolder GetSentFolder (ImapClient client, CancellationToken cancellationT Using LINQ, you could simplify this down to something more like this: ```csharp -static string[] CommonSentFolderNames = { "Sent Items", "Sent Mail", /* maybe add some translated names */ }; +static string[] CommonSentFolderNames = { "Sent Items", "Sent Mail", "Sent Messages", /* maybe add some translated names */ }; static IFolder GetSentFolder (ImapClient client, CancellationToken cancellationToken) { var personal = client.GetFolder (client.PersonalNamespaces[0]); - + return personal.GetSubfolders (false, cancellationToken).FirstOrDefault (x => CommonSentFolderNames.Contains (x.Name)); } ``` -Another option might be to allow the user of your application to configure which folder he or she wants to use as their Sent folder, Drafts folder, Trash folder, etc. +Another option might be to allow the user of your application to configure which folder he or she wants to use as their +Sent folder, Drafts folder, Trash folder, etc. How you handle this is up to you. @@ -491,44 +501,49 @@ How you handle this is up to you. The first thing you'll need to do is fork MailKit to your own GitHub repository. For instructions on how to do that, see the section titled **Getting the Source Code**. -If you use [Xamarin Studio](http://xamarin.com/studio) or [MonoDevelop](http://monodevelop.com), all of the -solution files are configured with the coding style used by MailKit. If you use Visual Studio or some -other editor, please try to maintain the existing coding style as best as you can. +If you use [Visual Studio for Mac](https://visualstudio.microsoft.com/vs/mac/) or [MonoDevelop](https://monodevelop.com), +all of the solution files are configured with the coding style used by MailKit. If you use Visual Studio on Windows +or some other editor, please try to maintain the existing coding style as best as you can. Once you've got some changes that you'd like to submit upstream to the official MailKit repository, -simply send me a **Pull Request** and I will try to review your changes in a timely manner. +send me a **Pull Request** and I will try to review your changes in a timely manner. If you'd like to contribute but don't have any particular features in mind to work on, check out the issue tracker and look for something that might pique your interest! -## Donate - -MailKit is a personal open source project that I have put thousands of hours into perfecting with the -goal of making it not only the very best email framework for .NET, but the best email framework for -any programming language. I need your help to achieve this. - - - Click here to lend your support to MimeKit and MailKit by making a donation via pledgie.com! - - ## Reporting Bugs -Have a bug or a feature request? [Please open a new issue](https://github.com/jstedfast/MailKit/issues). +Have a bug or a feature request? Please open a new +[bug report](https://github.com/jstedfast/MailKit/issues/new?template=bug_report.md) +or +[feature request](https://github.com/jstedfast/MailKit/issues/new?template=feature_request.md). -Before opening a new issue, please search for existing issues to avoid submitting duplicates. +Before opening a new issue, please search through any [existing issues](https://github.com/jstedfast/MailKit/issues) +to avoid submitting duplicates. It may also be worth checking the +[FAQ](https://github.com/jstedfast/MailKit/blob/master/FAQ.md) for common questions that other developers +have had. If MailKit does not work with your mail server, please include a [protocol log](https://github.com/jstedfast/MailKit/blob/master/FAQ.md#ProtocolLog) in your bug report, otherwise there is nothing I can do to fix the problem. If you are getting an exception from somewhere within MailKit, don't just provide the `Exception.Message` -string. Please include the `Exception.StackTrace` as well. The `Message`, by itself, is useless. +string. Please include the `Exception.StackTrace` as well. The `Message`, by itself, is often useless. ## Documentation -API documentation can be found at [http://mimekit.net/docs](http://mimekit.net/docs). +API documentation can be found at [https://www.mimekit.net/docs](https://www.mimekit.net/docs). + +Some example snippets can be found in the [`Documentation/Examples`](https://github.com/jstedfast/MailKit/tree/master/Documentation/Examples) directory. + +Sample applications can be found in the [`samples`](https://github.com/jstedfast/MailKit/tree/master/samples) directory. + +A copy of the XML-formatted API reference documentation is also included in the NuGet package. + +## .NET Foundation + +MailKit is a [.NET Foundation](https://www.dotnetfoundation.org/projects) project. + +This project has adopted the code of conduct defined by the [Contributor Covenant](https://contributor-covenant.org/) to clarify expected behavior in our community. For more information, see the [.NET Foundation Code of Conduct](https://www.dotnetfoundation.org/code-of-conduct). -A copy of the xml formatted API documentation is also included in the NuGet and/or -Xamarin Component package. +General .NET OSS discussions: [.NET Foundation forums](https://forums.dotnetfoundation.org) diff --git a/RFCs.md b/RFCs.md new file mode 100644 index 0000000000..25dc053199 --- /dev/null +++ b/RFCs.md @@ -0,0 +1,122 @@ +### Specifications + +The following IETF specifications define the IMAP, POP3 and SMTP protocols: + +* [821](https://tools.ietf.org/html/rfc821): SIMPLE MAIL TRANSFER PROTOCOL +* [822](https://tools.ietf.org/html/rfc822): STANDARD FOR THE FORMAT OF ARPA INTERNET TEXT MESSAGES +* [974](https://tools.ietf.org/html/rfc974): MAIL ROUTING AND THE DOMAIN SYSTEM +* [1081](https://tools.ietf.org/html/rfc1081): Post Office Protocol - Version 3 +* [1123](https://tools.ietf.org/html/rfc1123): Requirements for Internet Hosts -- Application and Support +* [1225](https://tools.ietf.org/html/rfc1225): Post Office Protocol - Version 3 (Obsoletes rfc1081) +* [1425](https://tools.ietf.org/html/rfc1425): SMTP Service Extensions +* [1426](https://tools.ietf.org/html/rfc1426): SMTP Service Extension for 8bit-MIME transport +* [1460](https://tools.ietf.org/html/rfc1460): Post Office Protocol - Version 3 (Obsoletes rfc1225) +* [1651](https://tools.ietf.org/html/rfc1651): SMTP Service Extensions (Obsoletes rfc1425) +* [1652](https://tools.ietf.org/html/rfc1652): SMTP Service Extension for 8bit-MIME transport (Obsoletes rfc1426) +* [1653](https://tools.ietf.org/html/rfc1653): SMTP Service Extension for Message Size Declaration +* [1725](https://tools.ietf.org/html/rfc1725): Post Office Protocol - Version 3 (Obsoletes rfc1460) +* [1730](https://tools.ietf.org/html/rfc1730): INTERNET MESSAGE ACCESS PROTOCOL - VERSION 4 +* [1731](https://tools.ietf.org/html/rfc1731): IMAP4 Authentication Mechanisms +* [1734](https://tools.ietf.org/html/rfc1734): POP3 AUTHentication command +* [1830](https://tools.ietf.org/html/rfc1830): SMTP Service Extensions for Transmission of Large and Binary MIME Messages +* [1854](https://tools.ietf.org/html/rfc1854): SMTP Service Extension for Command Pipelining +* [1870](https://tools.ietf.org/html/rfc1870): SMTP Service Extension for Message Size Declaration (Obsoletes rfc1653) +* [1869](https://tools.ietf.org/html/rfc1869): SMTP Service Extensions +* [1891](https://tools.ietf.org/html/rfc1891): SMTP Service Extension for Delivery Status Notifications +* [1928](https://tools.ietf.org/html/rfc1928): SOCKS Protocol Version 5 +* [1929](https://tools.ietf.org/html/rfc1929): Username/Password Authentication for SOCKS V5 +* [1939](https://tools.ietf.org/html/rfc1939): Post Office Protocol - Version 3 (Obsoletes rfc1725) +* [1961](https://tools.ietf.org/html/rfc1961): GSS-API Authentication Method for SOCKS Version 5 +* [2034](https://tools.ietf.org/html/rfc2034): SMTP Service Extension for Returning Enhanced Error Codes +* [2060](https://tools.ietf.org/html/rfc2060): INTERNET MESSAGE ACCESS PROTOCOL - VERSION 4rev1 (Obsoletes rfc1730) +* [2086](https://tools.ietf.org/html/rfc2086): IMAP4 ACL extension +* [2087](https://tools.ietf.org/html/rfc2087): IMAP4 QUOTA extension +* [2088](https://tools.ietf.org/html/rfc2088): IMAP4 non-synchronizing literals +* [2095](https://tools.ietf.org/html/rfc2095): IMAP/POP AUTHorize Extension for Simple Challenge/Response +* [2177](https://tools.ietf.org/html/rfc2177): IMAP4 IDLE command +* [2193](https://tools.ietf.org/html/rfc2193): IMAP4 Mailbox Referrals +* [2195](https://tools.ietf.org/html/rfc2195): IMAP/POP AUTHorize Extension for Simple Challenge/Response (Obsoletes rfc2095) +* [2197](https://tools.ietf.org/html/rfc2197): SMTP Service Extension for Command Pipelining (Obsoletes rfc1854) +* [2221](https://tools.ietf.org/html/rfc2221): IMAP4 Login Referrals +* [2222](https://tools.ietf.org/html/rfc2222): Simple Authentication and Security Layer (SASL) +* [2245](https://tools.ietf.org/html/rfc2245): Anonymous SASL Mechanism +* [2342](https://tools.ietf.org/html/rfc2342): IMAP4 Namespace +* [2359](https://tools.ietf.org/html/rfc2359): IMAP4 UIDPLUS extension +* [2449](https://tools.ietf.org/html/rfc2449): POP3 Extension Mechanism (Updates rfc1939) +* [2487](https://tools.ietf.org/html/rfc2487): SMTP Service Extension for Secure SMTP over TLS +* [2554](https://tools.ietf.org/html/rfc2554): SMTP Service Extension for Authentication +* [2595](https://tools.ietf.org/html/rfc2595): Using TLS with IMAP, POP3 and ACAP +* [2683](https://tools.ietf.org/html/rfc2683): IMAP4 Implementation Recommendations +* [2821](https://tools.ietf.org/html/rfc2821): Simple Mail Transfer Protocol (Obsoletes rfc0821, rfc0974, rfc1869) (Updates rfc1123) +* [2822](https://tools.ietf.org/html/rfc2822): Internet Message Format (Obsoletes rfc0822) +* [2831](https://tools.ietf.org/html/rfc2831): Using Digest Authentication as a SASL Mechanism +* [2920](https://tools.ietf.org/html/rfc2920): SMTP Service Extension for Command Pipelining (Obsoletes rfc2197) +* [2971](https://tools.ietf.org/html/rfc2971): IMAP4 ID extension +* [3030](https://tools.ietf.org/html/rfc3030): SMTP Service Extensions for Transmission of Large and Binary MIME Messages (Obsoletes rfc1830) +* [3207](https://tools.ietf.org/html/rfc3207): SMTP Service Extension for Secure SMTP over Transport Layer Security (Obsoletes rfc2487) +* [3348](https://tools.ietf.org/html/rfc3348): The Internet Message Action Protocol (IMAP4) Child Mailbox Extension +* [3461](https://tools.ietf.org/html/rfc3461): Simple Mail Transfer Protocol (SMTP) Service Extension for Delivery Status Notifications (DSNs) (Obsoletes rfc1891) +* [3501](https://tools.ietf.org/html/rfc3501): INTERNET MESSAGE ACCESS PROTOCOL - VERSION 4rev1 (Obsoletes rfc2060) +* [3502](https://tools.ietf.org/html/rfc3502): Internet Message Access Protocol (IMAP) - MULTIAPPEND Extension +* [3516](https://tools.ietf.org/html/rfc3516): IMAP4 Binary Content Extension +* [3691](https://tools.ietf.org/html/rfc3691): Handle System Namespace and Service Definition +* [4013](https://tools.ietf.org/html/rfc4013): SASLprep: Stringprep Profile for User Names and Passwords +* [4314](https://tools.ietf.org/html/rfc4314): IMAP4 Access Control List (ACL) Extension (Obsoletes rfc2086) +* [4315](https://tools.ietf.org/html/rfc4315): Internet Message Access Protocol (IMAP) - UIDPLUS extension (Obsoletes rfc2359) +* [4466](https://tools.ietf.org/html/rfc4466): Collected Extensions to IMAP4 ABNF (Updates rfc2088, rfc2342, rfc3501, rfc3502, rfc3516) +* [4469](https://tools.ietf.org/html/rfc4469): Internet Message Access Protocol (IMAP) CATENATE Extension (Updates rfc3501, rfc3502) +* [4505](https://tools.ietf.org/html/rfc4505): Anonymous Simple Authentication and Security Layer (SASL) Mechanism (Obsoletes rfc2245) +* [4551](https://tools.ietf.org/html/rfc4551): IMAP Extension for Conditional STORE Operation or Quick Flag Changes Resynchronization (Updates rfc3501) +* [4616](https://tools.ietf.org/html/rfc4616): The PLAIN Simple Authentication and Security Layer (SASL) Mechanism (Updates rfc2595) +* [4731](https://tools.ietf.org/html/rfc4731): IMAP4 Extension to SEARCH Command for Controlling What Kind of Information Is Returned +* [4952](https://tools.ietf.org/html/rfc4952): Overview and Framework for Internationalized Email +* [4959](https://tools.ietf.org/html/rfc4959): IMAP Extension for Simple Authentication and Security Layer (SASL) Initial Client Response +* [4978](https://tools.ietf.org/html/rfc4978): The IMAP COMPRESS Extension +* [5032](https://tools.ietf.org/html/rfc5032): WITHIN Search Extension to the IMAP Protocol (Updates rfc3501) +* [5161](https://tools.ietf.org/html/rfc5161): The IMAP ENABLE Extension +* [5162](https://tools.ietf.org/html/rfc5162): IMAP4 Extensions for Quick Mailbox Resynchronization +* [5182](https://tools.ietf.org/html/rfc5182): IMAP Extension for Referencing the Last SEARCH Result (Updates rfc3501) +* [5255](https://tools.ietf.org/html/rfc5255): Internet Message Access Protocol Internationalization +* [5256](https://tools.ietf.org/html/rfc5256): Internet Message Access Protocol - SORT and THREAD Extensions +* [5257](https://tools.ietf.org/html/rfc5257): Internet Message Access Protocol - ANNOTATE Extension +* [5258](https://tools.ietf.org/html/rfc5258): Internet Message Access Protocol version 4 - LIST Command Extensions (Obsoletes rfc3348) (Updates rfc2193) +* [5259](https://tools.ietf.org/html/rfc5259): Internet Message Access Protocol - CONVERT Extension +* [5267](https://tools.ietf.org/html/rfc5267): Contexts for IMAP4 +* [5321](https://tools.ietf.org/html/rfc5321): Simple Mail Transfer Protocol (Obsoletes rfc2821) (Updates rfc1123) +* [5336](https://tools.ietf.org/html/rfc5336): SMTP Extension for Internationalized Email Addresses (Updates rfc2821, rfc2822, rfc4952) +* [5464](https://tools.ietf.org/html/rfc5464): The IMAP METADATA Extension +* [5465](https://tools.ietf.org/html/rfc5465): The IMAP NOTIFY Extension (Updates rfc5267) +* [5466](https://tools.ietf.org/html/rfc5466): IMAP4 Extension for Named Searches (Filters) +* [5530](https://tools.ietf.org/html/rfc5530): IMAP Response Codes +* [5721](https://tools.ietf.org/html/rfc5721): POP3 Support for UTF-8 +* [5738](https://tools.ietf.org/html/rfc5738): IMAP Support for UTF-8 (Updates rfc3501) +* [5788](https://tools.ietf.org/html/rfc5788): IMAP4 Keyword Registry +* [5801](https://tools.ietf.org/html/rfc5801): Using Generic Security Service Application Program Interface (GSS-API) Mechanisms in Simple Authentication and Security Layer (SASL): The GS2 Mechanism Family +* [5802](https://tools.ietf.org/html/rfc5802): Salted Challenge Response Authentication Mechanism (SCRAM) SASL and GSS-API Mechanisms +* [5819](https://tools.ietf.org/html/rfc5819): IMAP4 Extension for Returning STATUS Information in Extended LIST +* [5957](https://tools.ietf.org/html/rfc5957): Display-Based Address Sorting for the IMAP4 SORT Extension (Updates rfc5256) +* [6154](https://tools.ietf.org/html/rfc6154): IMAP LIST Extension for Special-Use Mailboxes +* [6203](https://tools.ietf.org/html/rfc6203): IMAP4 Extension for Fuzzy Search +* [6237](https://tools.ietf.org/html/rfc6237): IMAP4 Multimailbox SEARCH Extension (Obsoletes rfc4466) +* [6531](https://tools.ietf.org/html/rfc6531): SMTP Extension for Internationalized Email (Obsoletes rfc5336) +* [6851](https://tools.ietf.org/html/rfc6851): Internet Message Access Protocol (IMAP) - MOVE Extension +* [6855](https://tools.ietf.org/html/rfc6855): IMAP Support for UTF-8 (Obsoletes rfc5738) +* [6856](https://tools.ietf.org/html/rfc6856): Post Office Protocol Version 3 (POP3) Support for UTF-8 (Obsoletes rfc5721) +* [7162](https://tools.ietf.org/html/rfc7162): IMAP Extensions: Quick Flag Changes Resynchronization (CONDSTORE) and Quick Mailbox Resynchronization (QRESYNC) (Obsoletes rfc4551, rfc5162) (Updates rfc2683) +* [7377](https://tools.ietf.org/html/rfc7377): IMAP4 Multimailbox SEARCH Extension (Obsoletes rfc6237) (Updates rfc4466) +* [7628](https://tools.ietf.org/html/rfc7628): A Set of Simple Authentication and Security Layer (SASL) Mechanisms for OAuth +* [7677](https://tools.ietf.org/html/rfc7677): SCRAM-SHA-256 and SCRAM-SHA-256-PLUS Simple Authentication and Security Layer (SASL) Mechanisms (Updates rfc5802) +* [7888](https://tools.ietf.org/html/rfc7888): IMAP4 Non-synchronizing Literals (Obsoletes rfc2088) +* [7889](https://tools.ietf.org/html/rfc7889): The IMAP APPENDLIMIT Extension +* [8437](https://tools.ietf.org/html/rfc8437): IMAP UNAUTHENTICATE Extension for Connection Reuse (Updates rfc3501) +* [8438](https://tools.ietf.org/html/rfc8438): IMAP Extension for STATUS=SIZE +* [8440](https://tools.ietf.org/html/rfc8440): IMAP4 Extension for Returning MYRIGHTS Information in Extended LIST +* [8457](https://tools.ietf.org/html/rfc8457): IMAP "$Important" Keyword and "\Important" Special-Use Attribute +* [8474](https://tools.ietf.org/html/rfc8474): IMAP Extension for Object Identifiers +* [8508](https://tools.ietf.org/html/rfc8508): IMAP REPLACE Extension (Updates rfc3501) +* [8514](https://tools.ietf.org/html/rfc8514): Internet Message Access Protocol (IMAP) - SAVEDATE Extension +* [8689](https://tools.ietf.org/html/rfc8689): SMTP Require TLS Option +* [8970](https://tools.ietf.org/html/rfc8970): IMAP4 Extension: Message Preview Generation +* [9051](https://tools.ietf.org/html/rfc9051): Internet Message Access Protocol (IMAP) - Version 4rev2 +* [9208](https://tools.ietf.org/html/rfc9208): IMAP QUOTA Extension (Obsoletes rfc2087) +* [9394](https://tools.ietf.org/html/rfc9394): IMAP PARTIAL Extension for Paged SEARCH and FETCH diff --git a/ReleaseNotes.md b/ReleaseNotes.md index cadff35ed1..be9a15f300 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -1,22 +1,978 @@ # Release Notes -### MailKit 1.16.1 - -* Properly handle a NIL body-fld-params token for body-part-mpart. (issue #503) - -### MailKit 1.16.0 - -* Improved IMAP ENVELOPE parser to prevent exceptions when parsing invalid mailbox addresses. (issue #494) +## MailKit 4.17.0 (2026-05-26) + +* Updated nullability of ImapClient.Inbox (which will never return null). + (issue [#1996](https://github.com/jstedfast/MailKit/issues/1996)) +* Fixed IMAP's logic for ACL, LISTRIGHTS, MYRIGHTS, QUOTAROOT, QUOTA, + and METADATA response parsers to properly handle []'s in the folder name. + (issue [#2002](https://github.com/jstedfast/MailKit/issues/2002)) +* Updated System.Threading.Tasks.Extensions to v4.6.3. +* Updated System.Formats.Asn1 to v10.0.0 (instead of 10.0.2) for .NET 10. +* Bumped MimeKit dependency to 4.17.0. +* Code quality improvements. + +## MailKit 4.16.0 (2026-04-15) + +* SECURITY: Fixed protocol streams to reset internal buffers after upgrading to SSL/TLS. +* Dispose of the RandomNumberGenerator used in RC4.GenerateKey(). + (issue [#1989](https://github.com/jstedfast/MailKit/issues/1989)) +* Fixed calculation for number of needed bytes in SOCKS5 connect response. + (issue [#1993](https://github.com/jstedfast/MailKit/issues/1993)) +* Bumped MimeKit dependency to 4.16.0. + +## MailKit 4.15.1 (2026-03-04) + +* SECURITY: Bumped MimeKit to 4.15.1 for a security fix that prevents mailbox addresses from being allowed + to contain CRLF sequences which can be used to inject SMTP commands in the SmtpClient when it sends + `MAIL FROM` or `RCPT TO` commands. + +## MailKit 4.15.0 (2026-02-15) + +* Default the SmtpClient/Pop3Client/ImapClient.SslProtocols to the ServicePointManager.SecurityProtocol + value in .NET Framework (net4x). (issue [#1952](https://github.com/jstedfast/MailKit/issues/1952)) +* Added support for.NET 10. +* Marked IMailService.SslCipherAlgorithm, SslCipherStrength, SslHashAlgorithm, SslHashStrength, + SslKeyExchangeAlgorithm, and SslKeyExchangeStrength as Obsolete in .NET 10 in favor of the + IMailService.SslCipherSuite property (.NET 10 only). +* Bumped MimeKit dependency to 4.15.0. + +## MailKit 4.14.1 (2025-10-13) + +* Re-added NTLM to the list of mechanisms to try by default. + (issue [#1953](https://github.com/jstedfast/MailKit/issues/1953)) +* Fixed IMAP to treat '+' as a normal Atom token. + (issue [#1956](https://github.com/jstedfast/MailKit/issues/1956)) +* Added fallback logic for imap.strato.de if LIST doesn't return the INBOX. + (issue [#1957](https://github.com/jstedfast/MailKit/issues/1957)) + +## MailKit 4.14.0 (2025-09-28) + +* Updated MailService::GetSslClientAuthenticationOptions to be protected virtual. + (issue [#1931](https://github.com/jstedfast/MailKit/issues/1931)) +* Fixed initialization of SaslMechanism to lazily check if GSSAPI and/or NTLM are + supported (natively) to avoid undesirable error messages appearing in dotnet logs + on Linux systems. (issue [#1924](https://github.com/jstedfast/MailKit/issues/1924)) +* Bumped MimeKit dependency to 4.14.0. + +## MailKit 4.13.0 (2025-06-25) + +* Fixed tokenization of IMAP atom tokens that start with '+'. + (issue [#1914](https://github.com/jstedfast/MailKit/issues/1914)) +* Fixed the Imap/Pop3/Smtp clients to no longer fallback to using the + ServicePointManager.ServerCertificateValidationCallback method on .NET Core. + (issue [#1925](https://github.com/jstedfast/MailKit/issues/1925)) +* Bumped MimeKit dependency to 4.13.0. + +## MailKit 4.12.1 (2025-05-17) + +* Fixed authentication bugs introduced into 4.12.0 related to adding GSSAPI and native NTLM auth support + which causes problems on Linux/Mac. + (issue [#1910](https://github.com/jstedfast/MailKit/issues/1910)) + (issue [#1911](https://github.com/jstedfast/MailKit/issues/1911)) +* Reverted System.Threading.Tasks.Extensions dependency back to 4.6.2. +* Reverted System.Formats.Asn1 dependency back to 8.0.1. + +## MailKit 4.12.0 (2025-04-28) + +* Added support for native NTLM auth (using .NET Core's NegotiateAuthentication API). + This new class is called SaslMechanismNtlmNative and is the default NTLM mechanism + used by MailKit clients on net8.0+. +* Added support for the GSSAPI SASL mechanism for net8.0+. + (issue [#1249](https://github.com/jstedfast/MailKit/issues/1249)) +* Fixed MailFolder.FirstUnread to be initialized to -1 to indicate unknown. + (issue [#1898](https://github.com/jstedfast/MailKit/issues/1898)) +* Added support for non-compliant keywords that begin with '\'. + (issue [#1906](https://github.com/jstedfast/MailKit/issues/1906)) +* Added SearchQuery.HasKeywords/NotKeywords() overloads that take params instead of IEnumerable<string>. +* Bumped System.Threading.Tasks.Extensions from 4.6.2 to 4.6.3. +* Bumped System.Formats.Asn1 from 8.0.1 to 8.0.2. +* Bumped MimeKit dependency to 4.12.0. + +## MailKit 4.11.0 (2025-03-08) + +* Default MailService.SslProtocols to SslProtocols.None which is what the dotnet SslStream team recommends. + (As weird as it may seem, SslProtocols.None does not mean "don't support any SSL protocols", it means + don't override the default system settings.) +* Bumped MimeKit dependency to 4.11.0. + +## MailKit 4.10.0 (2025-01-26) + +* Work around a QQMail/Yandex IMAP BODYSTRUCTURE response for empty multipart. + (issue [#1861](https://github.com/jstedfast/MailKit/issues/1861)) +* Added exception documentation for methods in IMailFolder. + (issue [#1868](https://github.com/jstedfast/MailKit/issues/1868)) +* Added IMailFolder.CanOpen property that checks IMailFolder.Attributes for NoSelect and NonExistent flags. +* Bumped MimeKit dependency to 4.10.0. + +## MailKit 4.9.0 (2024-12-09) + +* Added an IMAP work-around for mail.ru which sometimes sends integer tokens as decimals in its responses. + (issue [#1838](https://github.com/jstedfast/MailKit/issues/1838)) +* Added a workaround for GMail IMAP BODY responses that include multipart expressions without any children + (e.g. `("ALTERNATIVE")`). (issue [#1841](https://github.com/jstedfast/MailKit/issues/1841)) +* Fixed default system proxy to handle null credentials and check if the targetUri is bypassed. + (issue [#1852](https://github.com/jstedfast/MailKit/issues/1852)) +* Dropped support for net6.0 (Microsoft support ended Nov 12, 2024). +* Bumped System.Threading.Tasks.Extensions dependency to 4.6.0. +* Bumped MimeKit dependency to 4.9.0. + +## MailKit 4.8.0 (2024-09-29) + +* Added a UniqueIdRange.SortOrder property. +* Updated the protocol log help link for ProtocolExceptions. + (issue [#1800](https://github.com/jstedfast/MailKit/issues/1800)) +* Fix SmtpClient.Dispose() when telemetry is configured. + (issue [#1816](https://github.com/jstedfast/MailKit/issues/1816)) +* Added ProxyClient.SystemProxy as a convenience property that wraps + the default system proxy (aka HttpClient.DefaultProxy) on net6.0+. + +## MailKit 4.7.1 (2024-07-12) + +* Fixed ImapClient and Pop3Client.Connect/Async() when OTEL is enabled. + (issue [#1765](https://github.com/jstedfast/MailKit/issues/1765)) +* Bumped MimeKit dependency to 4.7.1 to fix a denial of service security issue in the System.Formats.Asn1 + dependency. + +## MailKit 4.7.0 (2024-06-29) + +* Added Activity/Metrics for Imap/Pop3/SmtpClient. + (issue [#1499](https://github.com/jstedfast/MailKit/issues/1499)) +* Bumped MimeKit dependency to 4.7.0. + +## MailKit 4.6.0 (2024-05-17) + +* Swallow SMTP RSET exceptions. These obscure other Send/SendAsync exceptions. Fixes a regression + introduced in 4.4.0. (issue [#1748](https://github.com/jstedfast/MailKit/issues/1748)) +* Fixed ImapUtils.FormatInternalDate() to properly handle negative timezone offsets with non-zero minutes. + (issue [#1743](https://github.com/jstedfast/MailKit/pull/1753)) +* Bumped MimeKit dependency to 4.6.0. + +## MailKit 4.5.0 (2024-04-13) + +* Added a new SmtpClient.RequireTLS property to fix sending mail via Strato.de. + (issue [#1737](https://github.com/jstedfast/MailKit/issues/1737)) +* Fixed SmtpClient to track the most recent response from the SMTP server in order to include + it in SmtpProtocolExceptions caused by unexpected server disconnects to provide more context. + (issue [#1744](https://github.com/jstedfast/MailKit/issues/1744)) +* Bumped MimeKit dependency to 4.5.0. + +## MailKit 4.4.0 (2024-03-02) + +* Added net8.0 targets +* Split more sync/async logic to reduce allocations made by async state machines when + calling the synchronous public APIs instead of the async APIs. + (issue [#1335](https://github.com/jstedfast/MailKit/issues/1335)) +* Fixed logic for formatting IMAP FETCH HEADER.FIELDS.NOT corner case that was exposed by newly + added unit tests. +* Fixed SmtpClient to disconnect during Authenticate/Async on socket errors. +* Fixed SmtpClient's re-EHLO logic to disconnect on errors. +* Added workaround for Zoho IMAP servers returning MODSEQ -1. + (issue [#1686](https://github.com/jstedfast/MailKit/issues/1686)) +* Added workaround for some IMAP servers that use () instead of NIL for an unset Content-Location header + in the BODYSTRUCTURE response. + (issue [#1700](https://github.com/jstedfast/MailKit/issues/1700)) +* Fixed an issue in the Socket.ConnectAsync logic that could result in unhandled exceptions on the + async thread if the ConnectAsync was cancelled. + (issue [#1703](https://github.com/jstedfast/MailKit/issues/1703)) +* Added work-around for Yandex IMAP GetBodyPart() response not including content. + (issue [#1708](https://github.com/jstedfast/MailKit/issues/1708)) +* Bumped MimeKit dependency to 4.4.0. + +## MailKit 4.3.0 (2023-11-11) + +* Fixed an ArgumentOutOfRangeException error in Fetch(int min, int max, ...) where min and max were greater + than folder.Count. (issue [#1640](https://github.com/jstedfast/MailKit/issues/1640)) +* Fixed parsing of IMAP FETCH (message/stream) responses with unsolicited FLAGS. +* Fixed support for the IMAP FILTERS extension. Previously this extension was not properly detected. +* When parsing IMAP CAPABILITIES, treat lone '+' tokens as atoms. + (issue [#1654](https://github.com/jstedfast/MailKit/issues/1654)) +* Bumped MimeKit dependency to 4.3.0. + +## MailKit 4.2.0 (2023-09-02) + +* Fixed a bug where the HttpProxyClient and HttpsProxyClient could end up reading the mail server greeting, + causing a connection failure for the ImapClient/Pop3Client/SmtpClient. + (issue [#1603](https://github.com/jstedfast/MailKit/issues/1603)) +* Parse IMAP quota values as ulongs instead of uints for GMail compatibility. + (issue [#1602](https://github.com/jstedfast/MailKit/issues/1602)) +* Added support for decoding SMTP DATA to the SmtpDataFilter. + (issue [#1607](https://github.com/jstedfast/MailKit/issues/1607)) +* Added a Pop3Client.Size property. (issue [#1623](https://github.com/jstedfast/MailKit/issues/1623)) +* Refactored more ImapClient commands to split sync/async implementations in order to improve + performance and reduce GC pressure. (issue [#1335](https://github.com/jstedfast/MailKit/issues/1335)) +* Added new IMailFolder.GetStream() methods that just take a uid/index and a BodyPart. +* Added IMailFolder.GetStream/Async() methods that just take a uid or index. +* Improved initial `List` capacity estimation for `Fetch (IList, ...)`. +* Fixed ByteArrayBuilder.TrimNewLine() to check array bounds properly. + (issue [#1634](https://github.com/jstedfast/MailKit/issues/1634)) +* Bumped MimeKit dependency to 4.2.0. + +## MailKit 4.1.0 (2023-06-17) + +* Fixed queueing logic for pipelining SMTP and POP3 commands. + (issue [#1568](https://github.com/jstedfast/MailKit/issues/1568)) +* Improve SslHandshakeException diagnostic messages. + (issue [#1554](https://github.com/jstedfast/MailKit/issues/1554)) +* Bumped System.Formats.Asn1 dependency to 7.0.0. +* Bumped MimeKit dependency to 4.1.0. + +## MailKit 4.0.0 (2023-04-15) + +* Marked the AccessRight and UniqueId structs as readonly. +* Fixed POP3 client logic to calculate the needed bytes before converting commands into into the output buffer. +* Ported to MimeKit v4.0 and BouncyCastle v2.1.1. + +## MailKit 3.6.0 (2023-03-04) + +* Decrement ImapFolder.Count when ImapClient receives an untagged EXPUNGE notification and emit a CountChanged event. + (issue [#1509](https://github.com/jstedfast/MailKit/issues/1509)) +* Avoid using the NAMESPACE command for Exchange 2003. + (issue [#1512](https://github.com/jstedfast/MailKit/issues/1512)) +* Added support for rfc8970 (IMAP4 Extension: Message Preview Generation). + +## MailKit 3.5.0 (2023-01-27) + +* Fixed bitmasking logic in SmtpClient.cs for deciding whether to use the BDAT command. +* Fixed HttpProxyClient to call GetConnectCommand() *before* connecting a socket to prevent memory leaks when + connecting fails. +* Improved the IMAP BODYSTRUCTURE parser to better handle broken responses. +* Fixed bug in Envelope.Parse/TryParse when given `(NIL NIL "" "localhost")` + (issue [#1471](https://github.com/jstedfast/MailKit/issues/1471)) +* Fixed SMTP client logic to calculate the needed bytes before converting commands into into the output buffer. + (issue [#1498](https://github.com/jstedfast/MailKit/issues/1498)) +* Fixed SmtpClient to replace _'s with -'s in the default LocalDomain string (used in HELO/EHLO commands). + (issue [#1501](https://github.com/jstedfast/MailKit/issues/1501)) + +## MailKit 3.4.3 (2022-11-25) + +* Fixed potential memory leaks in Pop3Client. +* Reverted SMTP pipelining of the DATA command. (issue [#1459](https://github.com/jstedfast/MailKit/issues/1459)) +* Fixed ImapFolder.Rename() to disallow renaming a folder to be a child of itself. +* Fixed SmtpStream.ReadResponse/Async() to handle buffers that do not contain a complete line. + (issue [#1467](https://github.com/jstedfast/MailKit/issues/1467)) + +## MailKit 3.4.2 (2022-10-24) + +* Fixed fetching of MessageSummaryItems.PreviewText if the octet count of the message body is 0. + (issue [#1430](https://github.com/jstedfast/MailKit/issues/1430)) +* Modified ImapFolder.Search(SearchOptions.None, query) work the same as ImapFolder.Search(query). + (issue [#1437](https://github.com/jstedfast/MailKit/issues/1437)) +* Improved performance of SmtpClient by reducing memory allocations and pipelining the DATA command when the PIPELINING + extension is available. +* Refactored sync and async SmtpClient APIs such that the synchronous APIs no longer call methods marked with async in order + to reduce AsyncMethodBuilder state machines/allocations. +* Modified SmtpClient to only send the ORCPT argument to RCPT TO if NOTIFY is specified. +* Improved performance of Pop3Client by reducing memory allocations. +* Refactored sync and async Pop3Client APIs such that the synchronous APIs no longer call methods marked with async in order + to reduce AsyncMethodBuilder state machines/allocations. +* Improved IMAP's BODY/BODYSTRUCTURE parser to be able to scan ahead multiple tokens in order to better handle syntactically + incorrect responses in a more graceful way. + (issue [#1446](https://github.com/jstedfast/MailKit/issues/1446)) +* Improved IMAP's ENVELOPE parser to handle ("Microsoft Exchange Server" NIL NIL ".MISSING-HOST-NAME.") in a more graceful way. + (issue [#1451](https://github.com/jstedfast/MailKit/issues/1451)) + +## MailKit 3.4.1 (2022-09-12) + +* Reverted the socket connection change to allow Socket.Connect() to do DNS lookups for us. Turns out, Socket.Connect() + doesn't iterate over all returned IP addresses until it finds an IP address that it can successfully connect to + for a given hostname which is what we need to do. + +## MailKit 3.4.0 (2022-09-05) + +* Fixed a bug that caused ImapFolder.Fetch/FetchAsync to throw TaskCanceledException instead of allowing + the correct exception to bubble up. (issue [#1415](https://github.com/jstedfast/MailKit/issues/1415)) +* Simplified socket connection logic to allow Socket.Connect() to do DNS lookups for us. +* Updated common mail server SSL certificates. +* Dropped net5.0 support. + +## MailKit 3.3.0 (2022-06-11) + +* Added work-around for IMAP BODYSTRUCTURE responses that have a NIL multipart body. + (issue [#1393](https://github.com/jstedfast/MailKit/issues/1393)) +* Considerably reduced memory overhead from compiler-generated async/await Tasks allocations in the IMAP + implementation (mostly focused on FETCH commands/responses). + (issue [#1335](https://github.com/jstedfast/MailKit/issues/1335)) +* Optimized FETCH response processing for the common case where FETCH responses are returned in sorted order. +* Fixed the IMAP Literal string reader to use UTF-8 with fallback to iso-8859-1 (previously just used iso-8859-1). +* Modified the IMAP ENVELOPE parser to combine ENVELOPE mailbox tokens if there are more than 4. + (issue [#1369](https://github.com/jstedfast/MailKit/issues/1369)) +* Prevent TypeLoadExceptions in the SmtpClient static .ctor by catching NotSupportedExceptions thrown by + IPGlobalProperties.GetIPGlobalProperties() on platforms like WASM. + (issue [#1381](https://github.com/jstedfast/MailKit/issues/1381)) +* Updated Google, GMX, and Yahoo! Mail SSL certificates. +* Dropped support for net452 and net461. +* Added support for net462. + +## MailKit 3.2.0 (2022-03-26) + +* Do not use ApplicationProtocols with SSL. (issue [#1352](https://github.com/jstedfast/MailKit/issues/1352)) +* Updated GMail, Yahoo, and Outlook.com certificates. +* Lazy-initialize MessageSummary.Keywords. This reduces memory usage when the client isn't requesting Flags/Keywords. +* Hard-cache some IMAP FETCH-related tokens in order to relieve GC pressure for commands like FETCH where there can + be a LOT of responses containing the same tokens over and over again. +* Converted some IMAP async Task methods to use ValueTask to reduce GC pressure. +* Reduced string allocations in the IMAP logic by avoiding use of ToUpperInvariant(). +* Added non-async implementations for ImapStream APIs to be used by the synchronous public APIs to avoid some async overhead. +* Reduce MemoryStream (and thus byte[]) allocations by using a new ByteArrayBuilder. +* Rewrote the IMAP CAPABILITY parser to avoid allocating strings. +* Fixed some cases where IMAP NIL tokens were not compared case insensitively. +* Always include the VERSION block in NTLM messages. (issue [#1340](https://github.com/jstedfast/MailKit/issues/1340)) +* Target .NET Framework v4.6.1 instead of v4.6 to match the changes in MimeKit. +* Capture the Socket timeout value in Read/WriteAsync() to have it in case of exceptions. + (issue [#1327](https://github.com/jstedfast/MailKit/issues/1327)) + +## MailKit 3.1.1 (2022-01-30) + +* Reduced string allocations in Pop3Engine's capability parser. +* Updated GMail and Outlook.com SSL certificates. +* Modified SmtpClient to try and use the system hostname in EHLO/HELO commands. + (issue [#1314](https://github.com/jstedfast/MailKit/issues/1314)) + +## MailKit 3.1.0 (2022-01-14) + +* Fixed NTLM to always prefer the supplied domain over the TargetName or TargetInfo.DomainName. + (issue [#582](https://github.com/jstedfast/MailKit/issues/582)) +* Updated GMail and Outlook.com SSL certificate info. +* Added a new SslCipherSuite property to each client that allows developers to get information + about the SSL/TLS cipher suite that was negotiated with the server. + (pull [#1312](https://github.com/jstedfast/MailKit/pull/1312)) +* Reduced string allocations in SmtpClient's EHLO capability parsing logic. +* Default ProtocolLogger.RedactSecrets to true for added added security. +* Added work-around for parsing malformed GMail ENVELOPE responses that reverse the name and address components + of the Sender address. (pull [#1319](https://github.com/jstedfast/MailKit/pull/1319)) +* Added net6.0 to the list of TargetFrameworks. + +## MailKit 3.0.0 (2021-12-11) + +* Removed APIs marked as \[Obsolete\] in 2.x. +* Simplify Fetch()/FetchAsync() APIs by using a new IFetchRequest parameter instead. Made previous APIs into + extension methods to aid in porting from 2.x. +* Replaced Add/Remove/SetFlags() APIs with Store()/StoreAsync() and simplified the APIs by using a new + IStoreFlagsRequest parameter. Made previous APIs into extension methods to aid in porting from 2.x. +* Replaced Add/Remove/SetLabels() APIs with Store()/StoreAsync() and simplified the APIs by using a new + IStoreLabelsRequest parameter. Made previous APIs into extension methods to aid in porting from 2.x. +* Simplify Append()/AppendAsync() APIs by using a new IAppendRequest parameter instead. Made previous APIs into + extension methods to aid in porting from 2.x. +* Simplify Replace()/ReplaceAsync() APIs by using a new IReplaceRequest parameter instead. Made previous APIs into + extension methods to aid in porting from 2.x. +* Updated SmtpClient.Send()/SendAsync() methods to return a string. + (issue [#1161](https://github.com/jstedfast/MailKit/issues/1161)) +* Added support for the SCRAM-SHA*-PLUS SASL mechanisms. + (issue [#950](https://github.com/jstedfast/MailKit/issues/950)) +* Added authzid support for SCRAM SASL mechanisms. +* Added support for the ANONYMOUS SASL mechanism. +* Added support for an HttpsProxyClient. (issue [#1251](https://github.com/jstedfast/MailKit/issues/1251)) +* Added AcceptedKeywords and PermanentKeywords to IMailFolder. + (issue [#1256](https://github.com/jstedfast/MailKit/issues/1256)) +* Rewrote NTLM support based on official specs. Now supports channel-binding and using the default system credentials. +* Modified ImapFolder.Fetch(int, int, ...) to shortcut if ImapFolder.Count == 0. +* Updated SmtpClient to append an ORCPT arg to RCPT TO commands and to hex-encode the ENVID parameter value. +* Improved/simplified logic for ranking SASL authentication mechanisms for each client. +* Added SaslMechanism.ChallengeAsync() to facilitate future SASL mechanisms that may need to make network requests + such as Kerberos/GSSAPI and perhaps even future/custom OAuth2 implementations. +* Always set SearchResults.Count/Min/Max properties if we can. +* Throw TimeoutException is case of a network time out. + (issue [#1269](https://github.com/jstedfast/MailKit/issues/1269)) +* Fixed parsing of IMAP flag lists to handle lowercase flag names. + (issue [#1277](https://github.com/jstedfast/MailKit/issues/1277)) +* Use OrdinalIgnoreCase when comparing "EARLIER" atom token. +* Avoid unnecessary string copies. (issue [#1292](https://github.com/jstedfast/MailKit/pull/1292)) +* Drop support for .NET 4.5 and replace it with .NET 4.5.2 +* Simplified event emissions based on EXISTS and EXPUNGED notifications. A CountChanged event is now *always* + emitted when the server sends an EXISTS notification. + (issue [#1288](https://github.com/jstedfast/MailKit/issues/1288)) + +## MailKit 2.15.0 (2021-08-18) + +* Use DebugType=full for .NET Framework v4.x. (issue [#1239](https://github.com/jstedfast/MailKit/issues/1239)) +* Updated GMail SSL certificate serial numbers and fingerprints. +* Small NTLM code improvements. + +## MailKit 2.14.0 (2021-07-28) + +* Added support for logging timestamps in the `ProtocolLogger` (see the `LogTimestamps` and `TimestampFormat` + properties on `ProtocolLogger`). +* Added support for automatically redacting user credentials in protocol logs. To enable this, set the + `ProtocolLogger.RedactSecrets` property to `true`. (issue [#1174](https://github.com/jstedfast/MailKit/issues/1174)) +* Added the GetMessageSizeAsync() method to the IMailSpool interface. + (issue [#1233](https://github.com/jstedfast/MailKit/issues/1233)) +* Added a work-around to the IMAP INTERNALDATE parser to handle invalid dates such as "00-Jan-0000 00:00:00 +0000" + which appears in Domino IMAP server responses, likely when the INTERNALDATE value is uninitialized in the database. + (issue [#1236](https://github.com/jstedfast/MailKit/issues/1236)) +* Make sure to dispose X509Certificates in .NET >= 4.6. +* Re-added NTLM as one of the default supported SASL mechanisms. +* Updated GMail SSL certificate serial numbers and fingerprints. + +## MailKit 2.13.0 (2021-06-12) + +* Added new properties to all clients to get SSL cipher/hash/protocol/key-exchange info. + (issue [#1175](https://github.com/jstedfast/MailKit/issues/1175)) +* Added support for GMail's WEBALERT resp-code. + (issue [#1214](https://github.com/jstedfast/MailKit/issues/1214)) +* Updated GMail SSL certificate serial numbers and fingerprints. + +## MailKit 2.12.0 (2021-05-12) + +* Fixed the .NET 5.0 build to include .NET 5.0-specific features. Previous releases incorrectly used + #if NET50 instead of #if NET5_0. (issue [#1140](https://github.com/jstedfast/MailKit/issues/1140)) +* Added support for NETStandard 2.1. (issue [#1181](https://github.com/jstedfast/MailKit/issues/1181)) +* .NETStandard 2.1 and .NET 5.0 versions of MailKit now use the newer SslStream.AuthenticateAsClientAsync() + methods that take SslClientAuthenticationOptions and CancellationToken arguments. In theory, this should + make upgrading a TCP/IP connection to SSL/TLS cancellable. Older .NET frameworks remain uncancellable for + this operation. +* Fixed a NullReferenceException bug in the NTLM SASL mechanism logic. +* Updated hard-coded SSL certificate serial numbers and fingerprints for common mail servers. + +## MailKit 2.11.1 (2021-03-16) + +* Added work-around for IMAP servers that do not correctly handle the ESEARCH `RETURN ()` syntax + the same as `RETURN (ALL)`. (issue [#1177](https://github.com/jstedfast/MailKit/issues/1177)) + +## MailKit 2.11.0 (2021-03-12) + +* Handle BAD responses to the NAMESPACE command for Exchange. + (issue [#1135](https://github.com/jstedfast/MailKit/issues/1135)) +* Added support for configuring SSL/TLS cipher algorithms (only available in the .NET 5.0 API). + (issue [#1140](https://github.com/jstedfast/MailKit/issues/1140)) +* Updated GMail and Yahoo! Mail SSL certificate info. +* Protect against NREs in NTLM authentication of no OSVersion is set. + (issue [#1148](https://github.com/jstedfast/MailKit/issues/1148)) +* Added work-around for hMailServer bug that doesn't accept seq-ranges in descending order. + (issue [#1150](https://github.com/jstedfast/MailKit/issues/1150)) +* Properly escape IPv6 addresses for Uri in order to allow Connect/Async methods to work with IPv6 addresses. + (issue [#1165](https://github.com/jstedfast/MailKit/issues/1165)) +* Added IsEncrypted and IsSigned properties to IMailService. + (issue [#1175](https://github.com/jstedfast/MailKit/issues/1175)) + +## MailKit 2.10.1 (2021-01-02) + +* A few NTLM improvements that I hope are correct. + +## MailKit 2.10.0 (2020-11-20) + +* Don't enable support for TLS v1.1 by default anymore. + (issue [#1077](https://github.com/jstedfast/MailKit/issues/1077)) +* Added support for the SCRAM-SHA-512 SASL mechanism. + (issue [#1097](https://github.com/jstedfast/MailKit/issues/1097)) +* Added support for the OAUTHBEARER SASL mechanism. +* Updated SSL certificate info for the common mail servers (GMail, outlook.com, Yahoo! Mail, etc). +* Improved the SslHandshakeException error message to report common mistakes like trying to initiate + an SSL connection on a non-SSL port. +* Improved IMAP's "Unexpected token" exception messages a bit +* Updated code to use ArrayPools from System.Buffers. + +## MailKit 2.9.0 (2020-09-12) + +* Refactored Connect/ConnectAsync() logic to set timeouts *before* calling SslStream.AuthenticateAsClient() + when connecting to an SSL-wrapped service. + (issue [#1059](https://github.com/jstedfast/MailKit/issues/1059)) +* Hardcode the value of SslProtocols.Tls13 for frameworks that do not support it and add it to the + client's default SslProtocols. This adds TLS v1.3 support, by default, for apps using .NETStandard2.0 + where the app project is built against a version of .NETCore that supports TLS v1.3. + (issue [#1058](https://github.com/jstedfast/MailKit/issues/1058)) +* Initialize IMAP SearchResults with the UIDVALIDITY value. + (issue [#1060](https://github.com/jstedfast/MailKit/issues/1060)) +* Make sure the ImapStream is not null (can be null if user calls Disconnect() causing IDLE to abort). + (issue [#1025](https://github.com/jstedfast/MailKit/issues/1025)) +* Case-insensitively match IMAP folder attribute flags (e.g. \HasNoChildren and \NoSelect). +* Added support for the IMAP SAVEDATE extension. +* Added support for detecting SMTP's REQUIRETLS extension. + +## MailKit 2.8.0 (2020-07-11) + +* Make sure to use the InvariantCulture when converting port values to a string. + (issue [#1040](https://github.com/jstedfast/MailKit/issues/1040)) +* Fixed other instances of string formatting for integer values to always use + CultureInfo.InvariantCulture. +* Added a work-around for broken IMAP servers that allow NIL message flags. + (issue [#1042](https://github.com/jstedfast/MailKit/issues/1042)) + +## MailKit 2.7.0 (2020-05-30) + +* Added a MessageSummary.Folder property and MessageThread.Message property + to allow developers to thread messages from multiple IMAP folders and be + able to figure out which folder each message belongs to. +* Added a work-around for IMAP servers that send a UIDNEXT response with a + value of '0'. (issue [#1010](https://github.com/jstedfast/MailKit/issues/1010)) +* Added an IMailFolder.Supports(FolderFeature) method so that developers can check + whether a feature is supported by the folder without needing a reference to the + corresponding ImapClient object in order to check the Capabilities. +* Fixed the HTTP proxy client to accept "200 OK" with an empty body as a successful + connection. (issue [#1015](https://github.com/jstedfast/MailKit/issues/1015)) +* Fixed the SOCKS5 proxy client to correctly send an authentication request. + (issue [#1019](https://github.com/jstedfast/MailKit/issues/1019)) +* Added support for customizable ProtocolLogger client/server prefixes. + (issue [#1024](https://github.com/jstedfast/MailKit/issues/1024)) +* Fixed an NRE in SslHandshakeException.Create() when running on Mono/Linux. +* Modified the SmtpClient to take advantage of the SMTPUTF8 extension for the + `MAIL FROM` and `RCPT TO` commands even if a `options.International` is not + explicitly set to `true` if any of the mailbox addresses are international + addresses. + (issue [#1026](https://github.com/jstedfast/MailKit/issues/1026)) +* Added support for a new Important SpecialFolder ([rfc8457](https://tools.ietf.org/html/rfc8457)). +* Added support for the IMAP REPLACE extension ([rfc8508](https://tools.ietf.org/html/rfc8508)). +* NuGet packages now include the portable pdb's. + +## MailKit 2.6.0 (2020-04-03) + +* Properly handle connection drops in SmtpClient.NoOp() and NoOpAsync() + methods. +* Improved default SSL certificate validation logic to be more secure + and to recognize the most commonly used mail servers even if their + Root CA Certificates are not available on the system. +* SslHandshakeException's Message has been improved to be based on the + errors reported in the ServerCertificateValidationCallback and also + now has 2 new X509Certificate properties which represent the + ServerCertificate and the RootCertificateAuthority in order to help + developers diagnose problems. + (issue [#1002](https://github.com/jstedfast/MailKit/issues/1002)) +* Improved the IMAP PreviewText to extract text from HTML bodies. + (issue [#1001](https://github.com/jstedfast/MailKit/issues/1001)) +* Renamed MessageSummaryItems.Id to MessageSummaryItems.EmailId to + better map to the property name used in the IMAP OBJECTID + specification. +* Updated NetworkStream.ReadAsync() and WriteAsync() methods to make use of + timeouts. (issue [#827](https://github.com/jstedfast/MailKit/issues/827)) + +## MailKit 2.5.2 (2020-03-14) + +* Added work-around for ENVELOPE responses with a NIL address token in an address-list. + (issue [#991](https://github.com/jstedfast/MailKit/issues/991)) + +## MailKit 2.5.1 (2020-02-15) + +* Fixed the IMAP ENVELOPE parser to have a more lenient fallback if it fails to be able to + parse the Message-Id token value. + (issue [#976](https://github.com/jstedfast/MailKit/issues/976)) +* Fixed MailService.DefaultServerCertificateValidationCallback() to compare certificates by + their hashes rather than via Object.Equals(). + (issue [#977](https://github.com/jstedfast/MailKit/issues/977)) +* Added work-around for IMAP servers that send `-1` as a line count or octet count in the + BODYSTRUCTURE response. + +## MailKit 2.5.0 (2020-01-18) + +* Ignore NIL tokens in the body-fld-lang token list. + (issue [#953](https://github.com/jstedfast/MailKit/issues/953)) +* Added logic to handle unexpected `` in untagged FETCH responses. + (issue [#954](https://github.com/jstedfast/MailKit/issues/954)) +* Added a way to override SmtpClient's preference for using BDAT vs DATA + via a new PreferSendAsBinaryData virtual property. +* Update SslHandshakeException message to mention the possibility of SSL/TLS + version mismatch. + (issue [#957](https://github.com/jstedfast/MailKit/issues/957)) +* Fixed ImapFolder.GetStreamsAsync() to use an async callback delegate. + (issue [#958](https://github.com/jstedfast/MailKit/issues/958)) +* Added protocol-specific interfaces that inherit from IMailFolder, + IMailStore, etc. + (issue [#960](https://github.com/jstedfast/MailKit/issues/960)) +* Maintain the STARTTLS capability bit flag after a STARTTLS command. +* Don't send the optional ANNOTATE parameter to SELECT/EXAMINE for + SUN IMAP servers (such as Apple's IMAP servers). + (issue [#970](https://github.com/jstedfast/MailKit/issues/970)) + +Note: Developers using ImapFolder.GetStreamsAsync() will need to update their code as +this release breaks API/ABI. + +## MailKit 2.4.1 (2019-11-10) + +* Don't use PublicSign on non-Windows NT machines when building. +* Work-around broken BODYSTRUCTUREs with `()` as a message/rfc822 body token. + (issue [#944](https://github.com/jstedfast/MailKit/issues/944)) +* Added work-around for an Exchange bug that forgets to quote folder names containing tabs. + (issue [#945](https://github.com/jstedfast/MailKit/issues/945)) +* Moved the SmtpDataFilter into the public API and updated the FAQ to show how to + use it when writing messages into an IIS "pickup directory". + (issue [#948](https://github.com/jstedfast/MailKit/issues/948)) + +## MailKit 2.4.0 (2019-11-02) + +* Added work-around for IMAP ENVELOPE responses that do not include an In-Reply-To token. + (issue [#932](https://github.com/jstedfast/MailKit/issues/932)) +* Dropped support for WindowsPhone/Universal v8.1. +* Added a net48 assembly to the NuGet package which supports TLS v1.3. +* Added work-around for Yandex IMAP servers to disconnect immediately upon `* BYE`. + (issue [#938](https://github.com/jstedfast/MailKit/issues/938)) +* Fixed ImapClient.Idle() and IdleAsync(). + (issue [#942](https://github.com/jstedfast/MailKit/issues/942)) +* Added work-around for Lotus Domino where it adds extra ()'s around some FETCH items. + (issue [#943](https://github.com/jstedfast/MailKit/issues/943)) + +## MailKit 2.3.2 (2019-10-12) + +* Fixed trimming delimiters from the end of IMAP folder names. +* Fixed fetching of IMAP PreviewText when message bodies do not contain any text parts. +* Fixed Pop3Client to never emit Authenticated events w/ null messages. +* Dropped SslProtocols.Tls (aka TLSv1.0) from the default SslProtocols used by IMAP, POP3 + and SMTP clients. To override this behavior, use the client.SslProtocols property + to set the preferred SslProtocol(s). +* Fixed ImapFolder.Search(string query) to properly encode the query string when the query + contains unicode characters. +* If an IMAP SEARCH fails due to BADCHARSET, retry the search query after flattening the + query strings into US-ASCII. This *may* fix issues such as + issue [#808](https://github.com/jstedfast/MailKit/issues/808). +* Added work-arounds for Exchange IMAP bugs causing it to send mal-formed body-fld-dsp + parameters. (issue [#919](https://github.com/jstedfast/MailKit/issues/919)) +* Go back to only using the BDAT command when the user is sending BINARYMIME in the SmtpClient. + (issue [#921](https://github.com/jstedfast/MailKit/issues/921)) + +## MailKit 2.3.1 (2019-09-08) + +* Fixed SmtpClient.Send*() to make sure never to add an extra CRLF sequence to the end of + messages when sending via the DATA command. + (issue [#895](https://github.com/jstedfast/MailKit/issues/895)) +* Added assemblies for net46 and net47 to the NuGet package. + +## MailKit 2.3.0 (2019-08-24) + +* Improved the default SSL/TLS certificate validation logic. +* Improved exception messages for the POP3 LIST and STAT commands. +* Modified Pop3Client to accept negative values for the 'octets' value in the STAT response. + (issue [#872](https://github.com/jstedfast/MailKit/issues/872)) +* Added work-around for IMAP BODYSTRUCTURE responses that treat multiparts as basic parts. + (issue [#878](https://github.com/jstedfast/MailKit/issues/878)) +* Added check to make sure that MD5 is supported by the runtime and automatically disable + support for CRAM-MD5 and DIGEST-MD5 SASL mechanisms when MD5 is not supported. +* Added a Stream property to ProtocolLogger. +* Fixed fetching of PreviewText items if the body's ContentTransferEncoding is NIL. + (issue [#881](https://github.com/jstedfast/MailKit/issues/881)) +* Improved processing of pipelined SMTP commands to provide better exception messages. + (issue [#883](https://github.com/jstedfast/MailKit/issues/883)) +* Modified SmtpClient.Send*() to not call MimeMessage.Prepare() if any DKIM or ARC headers + are present in order to avoid the potential risk of altering the message and breaking + the signatures within those headers. +* Added SmtpClient.SendCommand() and SendCommandAsync() to allow custom subclasses the + ability to send custom commands to the SMTP server. + (issue [#891](https://github.com/jstedfast/MailKit/issues/891)) +* Allow SmtpClient subclasses to override message preparation by overriding a new + SmtpClient.Prepare() method. + (issue [#891](https://github.com/jstedfast/MailKit/issues/891)) +* Improved ImapFolder's ModSeqChanged event to set the UniqueId property if available + in unsolicited FETCH notifications including a MODSEQ and UID value. +* Fixed the IMAP client logic to properly handle lower or mixed case IMAP tokens. + (issue [#893](https://github.com/jstedfast/MailKit/issues/893)) +* Added support for IMAP's ANNOTATE-EXPERIMENT-1 extension. + (issue [#818](https://github.com/jstedfast/MailKit/issues/818)) +* Always use the SMTP BDAT command instead of DATA if CHUNKING is supported. + (issue [#896](https://github.com/jstedfast/MailKit/issues/896)) +* Improved SmtpClient to include a SIZE= parameter in the MAIL FROM command if the + SIZE extension is supported. Progress reporting will now always have the expected + message size available as well. + +## MailKit 2.2.0 (2019-06-11) + +* Optimized MailKit's logic for breaking apart long IMAP commands for + GMail, Dovecot, and Yahoo! Mail. +* Fixed the IMAP stream tokenizer to properly handle UTF8 atom tokens. + (issue [#859](https://github.com/jstedfast/MailKit/issues/859)) +* Fixed IMAP search code to always handle untagged SEARCH responses even when + the response SHOULD be an untagged ESEARCH response. + (issue [#863](https://github.com/jstedfast/MailKit/issues/863)) +* Replaced SearchQuery.SentAfter with SentSince to be more consistent with IMAP + terminology. + +## MailKit 2.1.5 (2019-05-13) + +* Bumped the System.Net.Security dependency for security fixes (CVE-2017-0249). +* Reduced explicit nuget dependencies. +* Added a work-around for Microsoft Exchange IMAP servers that sometimes erroneously + respond with unneeded continuation responses. + (issue [#852](https://github.com/jstedfast/MailKit/issues/852)) +* Fixed the ImapClient to Stop looping over SASL mechanisms if the server disconnects us. + (issue [#851](https://github.com/jstedfast/MailKit/issues/851)) +* Added support for HTTP proxies. (issue [#847](https://github.com/jstedfast/MailKit/issues/847)) +* Fixed IMAP to properly handle EXPUNGE notifications during a FETCH request. + (issue [#850](https://github.com/jstedfast/MailKit/issues/850)) + +## MailKit 2.1.4 (2019-04-13) + +* Fixed ImapUtils.GetUniqueHeaders() to accept all valid header field name characters. + (issue [#806](https://github.com/jstedfast/MailKit/issues/806)) +* Catch all exceptions thrown in IdleComplete(). + (issue [#825](https://github.com/jstedfast/MailKit/issues/825)) +* Improved cancellability of IMAP, POP3 and SMTP clients when sending commands to the server. + (issue [#827](https://github.com/jstedfast/MailKit/issues/827)) +* Break apart IMAP commands with really long uid-sets. + (issue [#834](https://github.com/jstedfast/MailKit/issues/834)) +* Rewrote Connect logic to use Socket.Connect (IPAddress, int) instead of Connect (string, int) + in an attempt to fix [StackOverflow 87117](https://stackoverflow.com/q/55382267/87117) +* Fixed SmtpStream.ReadAheadAsync() to preserve remaining input. + (issue [#842](https://github.com/jstedfast/MailKit/issues/842)) + +## MailKit 2.1.3 (2019-02-24) + +* Fixed IMAP GetFolder() methods to match LIST responses case-insensitively. + (issue [#803](https://github.com/jstedfast/MailKit/issues/803)) +* Added a work-around to SmtpClient for a .NET 4.5.2 bug on Windows 7 SP1. + (issue [#814](https://github.com/jstedfast/MailKit/issues/814)) +* Added DeliveryStatusNotificationType and a property to SmtpClient to allow + developers to specify the `RET` parameter value to the `MAIL FROM` command. +* Fixed a number of locations in the code to clear password buffers after using + them. +* SmtpClient.Send() and SendAsync() methods that accept a FormatOptions argument + will no longer hide Bcc, Resent-Bcc, nor Content-Length headers when uploading + the raw message to the SMTP server. It is now up to callers to add these values + to their custom FormatOptions.HiddenHeaders property. + (issue [#360](https://github.com/jstedfast/MailKit/issues/360)) + +## MailKit 2.1.2 (2018-12-30) + +* Fixed a bug in SmtpDataFilter. (issue [#788](https://github.com/jstedfast/MailKit/issues/788)) +* Fixed ImapFolder.Sort() to always return the UIDs in the correct order. + (issue [#789](https://github.com/jstedfast/MailKit/issues/789)) +* Fixed *Client.ConnectAsync() to more reliably abort when the cancellation token is cancelled. + (issue [#798](https://github.com/jstedfast/MailKit/issues/798)) + +## MailKit 2.1.1 (2018-12-16) + +* Fixed ImapFolder.CopyTo() and ImapFolder.MoveTo() for IMAP servers that do not support UIDPLUS. + (issue [#787](https://github.com/jstedfast/MailKit/issues/787)) +* Fixed ImapClient.Connect() to capture authenticated state *before* calling OnConnected() so that + developers that call Authenticate() inside of the Connected event handler do not receive 2 Authenticated + events. (issue [#784](https://github.com/jstedfast/MailKit/issues/784)) + +## MailKit 2.1.0 (2018-12-01) + +* A number of fixes to bugs exposed in new unit tests for NTLM authentication. +* Made SmtpClient, Pop3Client, and ImapClient's Connect() methods truly cancellable as well + as made the underlying socket.Connect() call adhere to any specified client.Timeout value. +* Added support for connecting via a SOCKS4, SOCKS4a, or SOCKS5 proxy server. +* Fixed ImapClient's OnAuthenticated() method to protect against throwing an ArgumentNullException + when trying to emit the Authenticated event if the server did not supply any resp-code-text in + the OK response to the AUTHENTICATE command. (issue [#774](https://github.com/jstedfast/MailKit/issues/774)) +* Modified ImapFolder.Create() to handle [ALREADYEXISTS] resp-codes. +* Fixed ImapFolder.Create() for GMail when the isMessageFolder parameter is false (GMail doesn't handle + it when the client attempts to create a folder ending with a directory separator). +* Optimized ImapFolder's fallback for UID COPY command when UIDPLUS is not supported. +* Reduced string allocations in the Connect(Uri) wrapper. +* Added new ConnectedEventArgs and DisconnectedEventArgs that are used with the Connected and + Disconnected events to provide developers with even more useful information about what + server, port and SecureSocketOptions were used when connecting the client. +* Fixed SmtpClient to immediately throw stream reading exceptions instead of ignoring them. + (issue [#776](https://github.com/jstedfast/MailKit/issues/776)) +* Fixed ImapClient.GetFoldersAsync() to call ImapFolder.StatusAsync() instead of Status() + when StatusItems are specified. +* Changed ImapFolder.GetSubfolders() to return IList<IMailFolder> instead of IEnumerable<IMailFolder>. +* Fixed ImapClient's NAMESPACE parser - it had Shared and Other namespace ordering reversed. +* Fixed ImapFolder.Create() (for special-use) to only use unique uses if any were specified multiple times. +* Modified ImapFolder.Open() to allow devs to re-Open() a folder with the same access in case they + need to do this to work around an IMAP server bug(?). +* Fixed adding/removing/setting of GMail labels to use UTF-8 when enabled. +* Added support for the IMAP STATUS=SIZE extension which now provides a ImapFolder.Size property + that specifies how large a folder is (in bytes). Clients can request this information using the + StatusItems.Size enum with either ImapFolder.GetSubfolders() or ImapFolder.Status(). +* Added support for the IMAP OBJECTID extension. ImapFolder and IMessageSummary now both have + an Id property which is a globally unique identifier. IMessageSummary also now has a ThreadId + property which is a unique identifier for the message thread/conversation that the message + belongs to. This information can be retrieved for ImapFolders using ImapFolder.Status() with the + new StatusItems.MailboxId enum value. The IMessageSummary.Id and ThreadId properties have + the corresponding MessageSummaryItems enum values of Id and ThreadId, respectively. +* Added another work-around for bad GMail IMAP BODYSTRUCTURE responses. + (issue [#777](https://github.com/jstedfast/MailKit/issues/777)) +* Fixed all integer TryParse methods to use NumberStyles.None and CultureInfo.InvariantCulture. +* Added Connect() and ConnectAsync() overloads which accept a Stream instead of a Socket. +* All ImapFolder.MessageFlagsChanged, ModSeqChanged, and LabelsChanged events will now also be + followed by a MessageSummaryFetched event containing the combined information of those events. +* Added support for IMAP's NOTIFY extension. Many thanks to [Steffen Kieß](https://github.com/steffen-kiess) + for getting the ball rolling on this feature by implementing the necessary ImapEvent, ImapEventGroup, + and ImapMailboxFilter classes as well as the initial support. + +API Changes Since 2.0.x: + +* Obsoleted SearchQuery.HasCustomFlags() and SearchQuery.DoesNotHaveCustomFlags(). These are + now SearchQuery.HasKeywords() and SearchQuery.NotKeywords(), respectively. +* Obsoleted SearchQuery.DoesNotHaveFlags() in favor of SearchQuery.NotFlags(). +* Obsoleted the IMessageSummary.UserFlags property in favor of IMessageSummary.Keywords. +* Obsoleted the MessageFlagsChangedEventArgs.UserFlags property in favor of + MessageFlagsChangedEventArgs.Keywords. +* All IMailFolder.Fetch and IMailFolder.FetchAsync methods that took a HashSet<string> userFlags + argument now take an IEnumerable<string> keywords argument. Note: this only affects you if your + code used named method parameters (e.g. userFlags: myUserFlags). + +## MailKit 2.0.7 (2018-10-28) + +* Added a work-around for Exchange IMAP servers that send broken multipart BODYSTRUCTURE responses + without a `body-fld-dsp` token. +* Added support for detecting (but not using) the UNAUTHENTICATE IMAP extension. +* Reintroduced the Pop3Client.GetMessageCount() and GetMessageCountAsync() methods to allow developers + to poll POP3 servers for new messages. (issue [#762](https://github.com/jstedfast/MailKit/issues/762)) +* Fixed SmtpClient's status code logic to handle more than the expected error codes for the + `MAIL FROM` and `RCPT TO` commands. (issue [#764](https://github.com/jstedfast/MailKit/issues/764)) +* Added a work-around for IMAP servers that quote FLAGS responses. + (issue [#771](https://github.com/jstedfast/MailKit/issues/771)) +* Optimized SmtpClient's logic for byte-stuffing the message when writing it to the socket during + the `DATA` command. +* Added an `SslProtocols` property to IMailService (was already in MailService). +* Fixed the DIGEST-MD5 charset handling. +* Fixed a bug in the BodyPart.TryParse() method that could be used when serializing and deserializing + FETCH'd responses from an IMAP server. +* Fixed BodyPartCollection.IndexOf(Uri). +* Fixed Envelope.ToString() and TryParse() to properly deal with the rfc822 group address syntax. +* Fixed the ImapClient logic to properly handle parsing nested group addresses (not likely that + anyone would hit this). +* Improved ImapClient's state tracking so that it is possible to re-connect the ImapClient in the + Disconnected event handler. (issue [#770](https://github.com/jstedfast/MailKit/issues/770)) +* Fixed IMAP API's that take IList of UIDs or indexes to accept 0 UIDs/indexes. +* Fixed ImapClient's BODYSTRUCTURE parser to properly handle multiple body-extensions tokens. +* Fixed ImapClient to properly handle the `* PREAUTH` greeting when connecting to an IMAP server. + +## MailKit 2.0.6 (2018-08-04) + +* Fixed ImapFolder.GetSubfolders (StatusItems) to make sure that the child folders exist before + calling STATUS on them when the server does not support the LIST-STATUS command. +* Catch ArgumentExceptions when calling Encoding.GetEncoding(string). + (issue [#740](https://github.com/jstedfast/MailKit/issues/740)) +* Fixed parsing of IMAP threads where the root of a subtree is empty. + (issue [#739](https://github.com/jstedfast/MailKit/issues/739)) +* Added AuthorizationId property for PLAIN and DIGEST-MD5 SASL mechanisms. +* Added MessageSummaryItems.Headers enum to fetch all headers. + (issue [#738](https://github.com/jstedfast/MailKit/issues/738)) + +## MailKit 2.0.5 (2018-07-07) + +* When throwing AuthenticationException within SmtpClient, add an SmtpCommandException as the + InnerException property to help consumers diagnose authentication problems. + (issue [#717](https://github.com/jstedfast/MailKit/issues/717)) +* Added support for the authzid to the SASL PLAIN mechanism. +* Modified ProtocolLogger file constructor to support Shared Read and an Append/Overwrite option. + (issue [#730](https://github.com/jstedfast/MailKit/issues/730)) + +## MailKit 2.0.4 (2018-05-21) + +* Fixed SmtpClient to use the IPv4 literal if the socket is IPv4 address mapped to IPv6. + (issue [#704](https://github.com/jstedfast/MailKit/issues/704)) +* Updated SmtpClient and ImapFolder.Append to use FormatOptions.EnsureNewLine. + (MimeKit issue [#251](https://github.com/jstedfast/MimeKit/issues/251)) + +## MailKit 2.0.3 (2018-04-15) + +* Fixed IMAP IDLE support. +* Ignore unknown tokens in IMAP untagged FETCH responses such as XAOL.SPAM.REASON. + +## MailKit 2.0.2 (2018-03-18) + +* Added work-around for ProtonMail's IMAP server. (issue [#674](https://github.com/jstedfast/MailKit/issues/674)) +* Added work-around for IMAP servers that do not include msgid in the ENVELOPE response. + (issue [#669](https://github.com/jstedfast/MailKit/issues/669)) +* Added MessageSummaryItems.PreviewText to allow fetching a small preview of the message. + (issue [#650](https://github.com/jstedfast/MailKit/issues/650)) +* Added support for batch fetching IMAP message streams. + (issue [#650](https://github.com/jstedfast/MailKit/issues/650)) + +## MailKit 2.0.1 (2018-01-06) + +* Obsoleted all SaslMechanism constructors that took a Uri argument and replaced them + with variants that no longer require the Uri and instead take a NetworkCredential + or a set of strings for the user name and password. This simplifies authenticating + with OAuth 2.0: + +```csharp +var oauth2 = new SaslMechanismOAuth2 (username, auth_token); + +client.Authenticate (oauth2); +``` + +## MailKit 2.0.0 (2017-12-22) + +* Updated MailKit to fully support async IO instead of using Task.Run() wrappers. +* Fixed a resource leak when fetching IMAP body parts gets an exception. +* Fixed each of the Client.Connect() implementations to catch exceptions thrown by + IProtocolLogger.LogConnect(). +* Removed the ImapFolder.MessagesArrived event. +* Added new Authenticate() methods that take a SaslMechanism to avoid the need to + manipulate Client.AuthenticationMechanisms in order to tweak which SASL mechanisms + you'd like the client to use in Authenticate(). +* Added new SslHandshakeException with a helpful error message that can be thrown by + the Connect() methods. This replaces the obscure SocketExceptions previously thrown + by SslStream. +* Fixed support for the IMAP UTF8=ACCEPT extension. +* Improved ImapFolder.CommitStream() API to provide section, offset and length. +* Treat the SMTP X-EXPS capability in an EHLO response the same as AUTH. + (issue [#603](https://github.com/jstedfast/MailKit/issues/603)) +* Dropped support for .NET 4.0. + +Note: As of 2.0, XOAUTH2 is no longer in the list of SASL mechanisms that is tried +when using the Authenticate() methods that have existed pre-MailKit 2.0. +Instead, you must now use Authenticate(SaslMechanism, CancellationToken). + +An example usage might look like this: + +```csharp +// Note: The Uri isn't used except with ICredentials.GetCredential (Uri) so unless +// you implemented your own ICredentials class, the Uri is a dummy argument. +var uri = new Uri ("imap://imap.gmail.com"); +var oauth2 = new SaslMechanismOAuth2 (uri, username, auth_token); + +client.Authenticate (oauth2); +``` + +## MailKit 1.22.0 (2017-11-24) + +* Enable TLSv1.1 and 1.2 for .NETStandard. +* Read any remaining literal data after parsing headers. Fixes an issue when requesting + specific headers in an ImapFolder.Fetch() request if the server sends an extra newline. + +## MailKit 1.20.0 (2017-10-28) + +* Fixed UniqueIdRange.ToString() to always output a string in the form ${start}:${end} even if + start == end. (issue [#572](https://github.com/jstedfast/MailKit/issues/572)) + +## MailKit 1.18.1 (2017-09-03) + +* Gracefully handle IMAP COPYUID resp-codes without src or dest uid-set tokens. + (issue [#555](https://github.com/jstedfast/MailKit/issues/555)) +* Be more lenient with unquoted IMAP folder names containing ']'. + (issue [#557](https://github.com/jstedfast/MailKit/issues/557)) + +## MailKit 1.18.0 (2017-08-07) + +* Improved logic for cached FolderAttributes on ImapFolder objects. +* If/when the \NonExistent flag is present, reset ImapFolder state as it probably means + another client has deleted the folder. +* Added work-around for home.pl which sends an untagged `* [COPYUID ...]` response + without an `OK` (technically, the COPYUID resp-code should only appear in the tagged + response, but accept it anyway). + +## MailKit 1.16.2 (2017-07-01) + +* Added a leaveOpen param to the ProtocolLogger .ctor. + (issue [#506](https://github.com/jstedfast/MailKit/issues/506)) +* Added a CheckCertificateRevocation property on MailService. + (issue [#520](https://github.com/jstedfast/MailKit/issues/520)) +* Fixed ImapFolder to update the Count property and emit CountChanged when the IMAP server sends + an untagged VANISHED response. (issue [#521](https://github.com/jstedfast/MailKit/issues/521)) +* Fixed ImapEngine to properly handle converting character tokens into strings. + (issue [#522](https://github.com/jstedfast/MailKit/issues/522)) +* Fixed SmtpClient to properly handle DIGEST-MD5 auth errors in order to fall back to the next + authentication mechanism. +* Fixed Pop3Client to properly detect APOP tokens after arbitrary text. + (issue [#529](https://github.com/jstedfast/MailKit/issues/529)) +* Disabled NTLM authentication since it often doesn't work properly. + (issue [#532](https://github.com/jstedfast/MailKit/issues/532)) + +## MailKit 1.16.1 (2017-05-05) + +* Properly handle a NIL body-fld-params token for body-part-mpart. + (issue [#503](https://github.com/jstedfast/MailKit/issues/503)) + +## MailKit 1.16.0 (2017-04-21) + +* Improved IMAP ENVELOPE parser to prevent exceptions when parsing invalid mailbox addresses. + (issue [#494](https://github.com/jstedfast/MailKit/issues/494)) * Fixed UniqueId and UniqueIdRange to prevent developers from creating invalid UIDs and ranges. * Fixed ImapFolder.FetchStream() to properly emit MODSEQ changes if the server sends them. -* Fixed SmtpClient to call OnNoRecipientsAccepted even in the non-PIPELINE case. (issue #491) +* Fixed SmtpClient to call OnNoRecipientsAccepted even in the non-PIPELINE case. + (issue [#491](https://github.com/jstedfast/MailKit/issues/491)) -### MailKit 1.14.0 +## MailKit 1.14.0 (2017-04-09) -* Improved IMAP's BODYSTRUCTURE parser to sanitize the Content-Disposition values. (issue #486) +* Improved IMAP's BODYSTRUCTURE parser to sanitize the Content-Disposition values. + (issue [#486](https://github.com/jstedfast/MailKit/issues/486)) * Improved robustness of IMAP's BODYSTRUCTURE parser in cases where qstring tokens have unescaped - quotes. (issue #485) -* Fixed IMAP to properly handle NIL as a folder name in LIST, LSUB and STATUS responses. (issue #482) + quotes. (issue [#485](https://github.com/jstedfast/MailKit/issues/485)) +* Fixed IMAP to properly handle NIL as a folder name in LIST, LSUB and STATUS responses. + (issue [#482](https://github.com/jstedfast/MailKit/issues/482)) * Added ImapFolder.GetHeaders() to allow developers to download the entire set of message headers. * Added SMTP support for International Domain Names in email addresses used in the MAIL FROM and RCPT TO commands. @@ -25,30 +981,33 @@ the SMTPUTF8 extension. Instead, the local-part is passed through as UTF-8, leaving it up to the server to reject either the command or the message. This seems to provide the best interoperability. -### MailKit 1.12.0 +## MailKit 1.12.0 (2017-03-12) -* Allow an empty string text argument for SearchQuery.ContainsHeader(). (issue #451) +* Allow an empty string text argument for SearchQuery.ContainsHeader(). + (issue [#451](https://github.com/jstedfast/MailKit/issues/451)) * Fixed SaslMechanism.IsProhibited() logic to properly use logical ands. Thanks to Stefan Seering for this fix. -### MailKit 1.10.2 +## MailKit 1.10.2 (2017-01-28) * Added an IsAuthenticated property to IMailService. * Fixed the ImapFolder.Quota class to not be public. -### MailKit 1.10.1 +## MailKit 1.10.1 (2016-12-04) * Modified the ImapClient to always LIST the INBOX even if it is a namespace in order to get any flags set on it. -* Fixed ImapFolder to handle Quota Roots that do not match an existing folder. (issue #433) -* Added work-around for Courier-IMAP sending "* 0 FETCH ..." on flag changes. (issue #428) +* Fixed ImapFolder to handle Quota Roots that do not match an existing folder. + (issue [#433](https://github.com/jstedfast/MailKit/issues/433)) +* Added work-around for Courier-IMAP sending "* 0 FETCH ..." on flag changes. + (issue [#428](https://github.com/jstedfast/MailKit/issues/428)) * Updated MessageSorter to be smarter about validating arguments such that it will only check for IMessageSummary fields that it will *actually* need in order to perform the specified sort. * Fixed SmtpClient.Authenticate() to throw an AuthenticationException with a message from the SMTP server if available. -### MailKit 1.10.0 +## MailKit 1.10.0 (2016-10-31) * Added SearchQuery.Uids() to allow more powerful search expressions involving sets of uids. * Changed ImapClient.GetFolders() to return IList instead of IEnumerable. @@ -63,108 +1022,118 @@ * Improved SearchQuery optimization for IMAP. * Added SearchOptions.None. -### MailKit 1.8.1 +## MailKit 1.8.1 (2016-09-26) * Fixed the NuGet packages to reference MimeKit 1.8.0. * Added an SmtpClient.QueryCapabilitiesAfterAuthenticating property to work around broken SMTP servers where sending EHLO after a successful AUTH command incorrectly resets their authenticated state. -### MailKit 1.8.0 +## MailKit 1.8.0 (2016-09-26) * Added a new Search()/SearchAsync() to ImapFolder that take a raw query string. * Implemented support for the IMAP FILTERS extension and improved support for the METADATA extension. -* Fixed NTLM authentication support to use NTLMv2. (issue #397) +* Fixed NTLM authentication support to use NTLMv2. (issue [#397](https://github.com/jstedfast/MailKit/issues/397)) * Added support for IMAP's SEARCH=FUZZY relevancy scores. * Added an IMailFolder.ModSeqChanged event. * Added UniqueIdRange.All for convenience. -### MailKit 1.6.0 +## MailKit 1.6.0 (2016-09-11) * Added support for the new IMAP LITERAL- extension. * Added support for the new IMAP APPENDLIMIT extension. -* Fixed APOP authentication in the Pop3Client. (issue #395) +* Fixed APOP authentication in the Pop3Client. (issue [#395](https://github.com/jstedfast/MailKit/issues/395)) * Reset the SmtpClient's Capabilities after disconnecting. * Modified ImapFolder.Search() to return a UniqueIdSet for IMAP servers that do not support the ESEARCH extension (which already returns a UniqueIdSet). -* Added mail.shaw.ca to the list of SMTP servers that break when sending EHLO after AUTH. (issue #393) -* Work around broken POP3 servers that reply "+OK" instead of "+" in SASL negotiations. (issue #391) -* Modified the IMAP parser to properly allow "[" to appear within flag tokens. (issue #390) +* Added mail.shaw.ca to the list of SMTP servers that break when sending EHLO after AUTH. + (issue [#393](https://github.com/jstedfast/MailKit/issues/393)) +* Work around broken POP3 servers that reply "+OK" instead of "+" in SASL negotiations. + (issue [#391](https://github.com/jstedfast/MailKit/issues/391)) +* Modified the IMAP parser to properly allow "[" to appear within flag tokens. + (issue [#390](https://github.com/jstedfast/MailKit/issues/390)) -### MailKit 1.4.2.1 +## MailKit 1.4.2.1 (2016-08-16) * Fixed a regression in 1.4.2 where using a bad password in ImapClient.Authenticate() did not properly - throw an exception when using a SASL mechanism. (issue #383) + throw an exception when using a SASL mechanism. (issue [#383](https://github.com/jstedfast/MailKit/issues/383)) -### MailKit 1.4.2 +## MailKit 1.4.2 (2016-08-14) -* Properly initialize the private Uri fields in Connect() for Windows Universal 8.1. (issue #381, #382) +* Properly initialize the private Uri fields in Connect() for Windows Universal 8.1. + (issue [#381, #382](https://github.com/jstedfast/MailKit/issues/381, #382)) * Added SecuritySafeCritical attributes to try and match base Exception in case that matters. * Added missing GetObjectData() implementation to Pop3CommandException. * Strong-name the .NET Core assemblies. -* Make sure to process Alert resp-codes in ImapClient. (issue #377) +* Make sure to process Alert resp-codes in ImapClient. + (issue [#377](https://github.com/jstedfast/MailKit/issues/377)) -### MailKit 1.4.1 +## MailKit 1.4.1 (2016-07-17) * Updated the NTLM SASL mechanism to include a Windows OS version in the response if the server requests it (apparently this should only happen if the server is in debug mode). * Updated the IMAP BODYSTRUCTURE parser to try and work around BODYSTRUCTURE responses that do not properly encode the mime-type of a part where it only provides the media-subtype token - instead of both the media-type and media-subtype tokens. (issue #371) + instead of both the media-type and media-subtype tokens. + (issue [#371](https://github.com/jstedfast/MailKit/issues/371)) * Added smtp.dm.aliyun.com to the list of broken SMTP servers that failed to read the SMTP specifications and improperly reset their state after sending an EHLO command after - authenticating (which the specifications explicitly state the clients SHOULD do). (issue #370) + authenticating (which the specifications explicitly state the clients SHOULD do). + (issue [#370](https://github.com/jstedfast/MailKit/issues/370)) -### MailKit 1.4.0 +## MailKit 1.4.0 (2016-07-01) * Added support for .NET Core 1.0 -### MailKit 1.2.24 +## MailKit 1.2.24 (2016-06-16) -* Fixed logic for constructing the HELO command on WP8. (issue #351) +* Fixed logic for constructing the HELO command on WP8. (issue [#351](https://github.com/jstedfast/MailKit/issues/351)) * Modified ImapFolder.Search() to not send the optional CHARSET search param if the charset is US-ASCII. This way work around some broken IMAP servers that do not properly implement - support for the CHARSET parameter. (issue #348) + support for the CHARSET parameter. (issue [#348](https://github.com/jstedfast/MailKit/issues/348)) * Added more MailService methods to IMailService. -### MailKit 1.2.23 +## MailKit 1.2.23 (2016-05-22) -* Properly apply SecurityCriticalAttribute to GetObjectData() on custom Exceptions. (issue #340) +* Properly apply SecurityCriticalAttribute to GetObjectData() on custom Exceptions. + (issue [#340](https://github.com/jstedfast/MailKit/issues/340)) -### MailKit 1.2.22 +## MailKit 1.2.22 (2016-05-07) * Updated IMAP BODY parser to handle a NIL media type by treating it as "application". * Updated IMAP SEARCH response parser to work around search-return-data pairs within parens. -* Added a missing SmtpStatusCode enum value for code 555. (issue #327) +* Added a missing SmtpStatusCode enum value for code 555. + (issue [#327](https://github.com/jstedfast/MailKit/issues/327)) * Opened up more of the SearchQuery API to make it possible to serialize/deserialize via JSON. - (issue #331) + (issue [#331](https://github.com/jstedfast/MailKit/issues/331)) * Updated to reference BouncyCastle via NuGet.org packages rather than via project references. -### MailKit 1.2.21 +## MailKit 1.2.21 (2016-03-13) * Replaced SmtpClient's virtual ProcessRcptToResponse() method with OnRecipientAccepted() - and OnRecipientNotAccepted(). (issue #309) + and OnRecipientNotAccepted(). (issue [#309](https://github.com/jstedfast/MailKit/issues/309)) * Added MailService.DefaultServerCertificateValidationCallback() which accepts all self-signed certificates (a common operation that consumers want). * Fixed encoding and decoding of IMAP folder names that include surrogate pairs. * Fixed IMAP SEARCH logic for X-GM-LABELS. -### MailKit 1.2.20 +## MailKit 1.2.20 (2016-02-28) * Added a work-around for GoDaddy's ASP.NET web host which does not support the iso-8859-1 System.Text.Encoding (used as a fallback encoding within MailKit) by falling back to Windows-1252 instead. * Improved NTLM support. -### MailKit 1.2.19 +## MailKit 1.2.19 (2016-02-13) * Added support for the SMTP VRFY and EXPN commands. -### MailKit 1.2.18 +## MailKit 1.2.18 (2016-01-29) * If the IMAP server sends a `* ID NIL` response, return null for ImapClient.Identify(). -* Allow developers to override the charset used when authenticating. (issue #292) +* Allow developers to override the charset used when authenticating. + (issue [#292](https://github.com/jstedfast/MailKit/issues/292)) -### MailKit 1.2.17 +## MailKit 1.2.17 (2016-01-24) * Exposed MailKit.Search.OrderByType and MailKit.Search.SortOrder to the public API. * Modified IMailFolder.CopyTo() and MoveTo() to return a UniqueIdMap instead of a UniqueIdSet. @@ -179,73 +1148,78 @@ false instead of forcing developers to pass in a value. * Updated the IMAP, POP3 and SMTP clients to be stricter with validating SSL certificates. -### MailKit 1.2.16 +## MailKit 1.2.16 (2016-01-01) * Added support for the SCRAM-SHA-256 SASL mechanism. * Added support for the CREATE-SPECIAL-USE IMAP extension. * Added support for the METADATA IMAP extension. * Added support for the LIST-STATUS IMAP extension. -### MailKit 1.2.15 +## MailKit 1.2.15 (2015-11-29) * Be more forgiving during SASL auth when a POP3 server sends unexpected text after a + response. - (issue #268) + (issue [#268](https://github.com/jstedfast/MailKit/issues/268)) -### MailKit 1.2.14 +## MailKit 1.2.14 (2015-11-22) -* Fixed ImapFolder.Search() to not capitalize the date strings in date queries. (issue #252) +* Fixed ImapFolder.Search() to not capitalize the date strings in date queries. + (issue [#252](https://github.com/jstedfast/MailKit/issues/252)) * Fixed filtering logic in ImapFolder.GetSubfolders() to not filter out subfolders named Inbox. - (issue #255) + (issue [#255](https://github.com/jstedfast/MailKit/issues/255)) * Exposed SmtpClient.ProcessRcptToResponse() as virtual protected to allow subclasses to override - error handling. (issue #256) -* Modified SmtpCommandException .ctors to be public and fixed serialization logic. (issue #257) + error handling. (issue [#256](https://github.com/jstedfast/MailKit/issues/256)) +* Modified SmtpCommandException .ctors to be public and fixed serialization logic. + (issue [#257](https://github.com/jstedfast/MailKit/issues/257)) * Added workaround for broken smtp.sina.com mail server. * Throw a custom ImapProtocolException on "* BYE" during connection instead of "unexpected token". - (issue #262) + (issue [#262](https://github.com/jstedfast/MailKit/issues/262)) -### MailKit 1.2.13 +## MailKit 1.2.13 (2015-10-18) * Fixed SmtpClient to not double dispose the socket. * Added a BodyPartVisitor class. -* Fixed ImapFolder to allow NIL tokens for body parts. (issue #244) +* Fixed ImapFolder to allow NIL tokens for body parts. (issue [#244](https://github.com/jstedfast/MailKit/issues/244)) -### MailKit 1.2.12 +## MailKit 1.2.12 (2015-09-20) * Allow developers to specify a local IPEndPoint to use for connecting to remote servers. - (issue #247) -* Added support for NIL GMail labels. (issue #244) + (issue [#247](https://github.com/jstedfast/MailKit/issues/247)) +* Added support for NIL GMail labels. (issue [#244](https://github.com/jstedfast/MailKit/issues/244)) -### MailKit 1.2.11.1 +## MailKit 1.2.11.1 (2015-09-08) * Fixed ImapFolder.GetSubfolders() to work with Yahoo! Mail and other IMAP servers that - do not use the canonical INBOX naming convention for the INBOX folder. (issue #242) + do not use the canonical INBOX naming convention for the INBOX folder. + (issue [#242](https://github.com/jstedfast/MailKit/issues/242)) -### MailKit 1.2.11 +## MailKit 1.2.11 (2015-09-06) -* Fixed SmtpStream logic for determining if a call to ReadAhead() is needed. (issue #232) +* Fixed SmtpStream logic for determining if a call to ReadAhead() is needed. + (issue [#232](https://github.com/jstedfast/MailKit/issues/232)) * Fixed ImapFolder.Close() to change the state to Closed even if the IMAP server does not support the UNSELECT command. * Allow the UIDVALIDITY argument to the COPYUID and APPENDUID resp-codes to be 0 even though - that value is illegal. Improves compatibility with SmarterMail. (issue #240) + that value is illegal. Improves compatibility with SmarterMail. + (issue [#240](https://github.com/jstedfast/MailKit/issues/240)) -### MailKit 1.2.10 +## MailKit 1.2.10 (2015-08-16) * Added an SslProtocols property to ImapClient, Pop3Client, and SmtpClient to allow developers to override which SSL protocols are to be allowed for SSL connections. - (issue #229) + (issue [#229](https://github.com/jstedfast/MailKit/issues/229)) * Added a work-around for GMail IMAP (and other IMAP servers) that sometimes send an - illegal MODSEQ value of 0. (issue #228) + illegal MODSEQ value of 0. (issue [#228](https://github.com/jstedfast/MailKit/issues/228)) -### MailKit 1.2.9 +## MailKit 1.2.9 (2015-08-08) -* Fixed ImapFolder.Append() methods to make sure to encode the message with +* Fixed ImapFolder.Append() methods to make sure to encode the message with `` line endings. * Added UniqueId.Invalid that can be used for error conditions. * Added UniqueId.IsValid property to check that the UniqueId is valid. * Added Opened and Closed events to IMailFolder. * Fixed the QRESYNC version of the IMailFolder.Open() method to take a uint uidValidity instead of a UniqueId uidValidity argument for consistency. -* Updated MessageSorter.Sort() to be an extension method and added a List overload. +* Updated MessageSorter.Sort() to be an extension method and added a List<T> overload. * Updated MessageThreader.Thread() to be extension methods (required reordering of args). * Merged ISortable and IThreadable interfaces into IMessageSummary in order to remove duplicated properties and simplify things. @@ -253,44 +1227,44 @@ * Modified IMessageSummary.UniqueId to no longer be nullable. * Added TextBody, HtmlBody, BodyParts and Attachments properties to IMessageSummary. * Modified the IMAP parser to allow NIL for the Content-Type and subtype strings in - BODY and BODYSTRUCTURE values even though it is illegal. (issue #226) + BODY and BODYSTRUCTURE values even though it is illegal. (issue [#226](https://github.com/jstedfast/MailKit/issues/226)) * Modified the IMAP parser to properly handle Message-Id tokens that are not properly - encapsulated within angle brackets. (issue #224) + encapsulated within angle brackets. (issue [#224](https://github.com/jstedfast/MailKit/issues/224)) * Fixed IMAP to properly deal with folder names that contained unescaped square brackets. - (issue #222) + (issue [#222](https://github.com/jstedfast/MailKit/issues/222)) -### MailKit 1.2.8 +## MailKit 1.2.8 (2015-07-19) * Fixed ImapFolder to dispose the temporary streams used in GetMessage and GetBodyPart. * Added a MessageNotFoundException. * Added an ImapCommandResponse property to ImapCommandException. * Fixed SmtpClient to filter out duplicate recipient addresses in RCPT TO. -* Modified MessageSorter/Threader to take IList arguments instead of OrderBy[]. +* Modified MessageSorter/Threader to take IList<OrderBy> arguments instead of OrderBy[]. * Added support for parsing group addresses in IMAP ENVELOPE responses. -* Disable SASL-IR support for the LOGIN mechanism. (issue #216) +* Disable SASL-IR support for the LOGIN mechanism. (issue [#216](https://github.com/jstedfast/MailKit/issues/216)) * Capture whether or not the IMAP server supports the I18NLEVEL and LANGUAGE extensions. -### MailKit 1.2.7 +## MailKit 1.2.7 (2015-07-06) * Fixed ImapFolder.Rename() to properly emit the Renamed event for child folders as well. * Fixed ImapFolder.Fetch() to always fill in the Headers property when requesting specific - headers even if the server replies with an empty list. (issue #210) + headers even if the server replies with an empty list. (issue [#210](https://github.com/jstedfast/MailKit/issues/210)) -### MailKit 1.2.6 +## MailKit 1.2.6 (2015-06-25) * Fixed UniqueIdSet.CopyTo() to work properly (also fixes LINQ usage). * Fixed ImapFolder.Status() where StatusItems.HighestModSeq is used. -### MailKit 1.2.5 +## MailKit 1.2.5 (2015-06-22) * Added support for extended IMAP search options (see the SearchOptions flags). * Added TryParse() convenience methods for UniqueIdSet, UniqueIdRange, and UniqueId. -* Added a workaround for a GMail IMAP BODYSTRUCTURE bug. (issue #205) +* Added a workaround for a GMail IMAP BODYSTRUCTURE bug. (issue [#205](https://github.com/jstedfast/MailKit/issues/205)) * Added a ProtocolLogger property for ImapClient, Pop3Client, and SmtpClient. * Fixed the ImapFolder.GetStream() methods that take a BodyPart to call the proper overload. -### MailKit 1.2.4 +## MailKit 1.2.4 (2015-06-14) * Updated SmtpClient to use MimeMessage.Prepare() instead of implementing its own logic. * Added a new ITransferProgress interface and updated IMAP, POP3 and SMTP methods to @@ -299,16 +1273,16 @@ extension. * Improved API documentation. -### MailKit 1.2.3 +## MailKit 1.2.3 (2015-06-01) * Fixed ImapFolder.AddFlags() to throw FolderNotOpenException if the folder is not - opened in read-write mode. (issue #202) + opened in read-write mode. (issue [#202](https://github.com/jstedfast/MailKit/issues/202)) * Fixed ImapFolder.GetMessage/BodyPart/Stream() to not modify a dictionary while - looping over it. (issue #201) + looping over it. (issue [#201](https://github.com/jstedfast/MailKit/issues/201)) * Fixed ImapFolder to throw FolderNotFoundException instead of ArgumentException when the command fails due to the folder not existing. -### MailKit 1.2.2 +## MailKit 1.2.2 (2015-05-31) * Added ImapClient.GetFolders(FolderNamespace, ...) to allow getting the full (recursive) list of folders for a particular namespace. @@ -318,72 +1292,72 @@ FolderNotOpenException as a more specific errors than InvalidOperationException. (Note: they all subclass InvalidOperationException so old code continues to work). * Added Pop3Client.GetStream() to allow fetching messages or headers as an unparsed - stream. (issue #198) + stream. (issue [#198](https://github.com/jstedfast/MailKit/issues/198)) * Fixed usage of Socket.Poll() to not loop 1000 times per second. * Added more ImapFolder.GetStream() overloads. * Added ImapFolder.CreateStream() and CommitStream() protected methods which are meant for subclasses that intend to implement caching. -### MailKit 1.2.1 +## MailKit 1.2.1 (2015-05-26) * Added hooks to allow subclassing ImapFolder. -### MailKit 1.2.0 +## MailKit 1.2.0 (2015-05-24) * Added new ImapFolder.GetStream() overloads that allow fetching only the TEXT stream. * Fixed ImapFolder.Search() to always treat the search results as UIDs even when the server (such as AOL) does not include the required UID tag in the - ESEARCH response. (issue #191) + ESEARCH response. (issue [#191](https://github.com/jstedfast/MailKit/issues/191)) * Fixed ImapClient to set the engine.Uri even for Windows*81 profiles (fixes - a NullReferenceException for the various Windows*81 profiles). (issue #192) + a NullReferenceException for the various Windows*81 profiles). (issue [#192](https://github.com/jstedfast/MailKit/issues/192)) * Work around a GMail bug where it does not quote flags containing []'s. - (issue #193) + (issue [#193](https://github.com/jstedfast/MailKit/issues/193)) * Fixed the IMAP code to accept GMail label names that start with a '+'. - (issue #195) + (issue [#195](https://github.com/jstedfast/MailKit/issues/195)) * Delay throwing ProtocolException due to an unexpected disconnect when reading responses to PIPELINE'd SMTP commands in case one of the responses to those commands contains an error code that might hint at why the server disconnected. - (issue #194) + (issue [#194](https://github.com/jstedfast/MailKit/issues/194)) -### MailKit 1.0.17 +## MailKit 1.0.17 (2015-05-12) * Fixed a STARTTLS regression in SmtpClient that was introduced in 1.0.15. - (issue #187) + (issue [#187](https://github.com/jstedfast/MailKit/issues/187)) -### MailKit 1.0.16 +## MailKit 1.0.16 (2015-05-10) * Modified the Pop3Client to immediately query for the message count once the client is authenticated. This allows the Pop3Client to now have a Count - property that replaces the need for calling GetMessageCount(). (issue #184) + property that replaces the need for calling GetMessageCount(). (issue [#184](https://github.com/jstedfast/MailKit/issues/184)) -### MailKit 1.0.15 +## MailKit 1.0.15 (2015-05-09) * Added SearchQuery.HeaderContains() and obsoleted SearchQuery.Header() for API consistency. * Added workaround for GMail's broken FETCH command parser that does not accept - aliases. (issue #183) + aliases. (issue [#183](https://github.com/jstedfast/MailKit/issues/183)) -### MailKit 1.0.14 +## MailKit 1.0.14 (2015-04-11) * Added a ServerCertificateValidationCallback property to all clients so that it is not necessary to set the global System.Net.ServicePointManager.ServerCertificateValidationCallback property. * Fixed MailService.Connect(Uri) to properly handle Uri's with Port value that - had not been explicitly set. (issue #170) + had not been explicitly set. (issue [#170](https://github.com/jstedfast/MailKit/issues/170)) * Added logic to properly handle MODSEQ-based search responses. - (issue #166 and issue #173) + (issue [#166 and issue #173](https://github.com/jstedfast/MailKit/issues/166 and issue #173)) * When an ImapClient gets disconnected, if an ImapFolder was in an opened state, update its state to closed to prevent confusion once the ImapClient is reconnected. * Fixed a bug in Pop3Client.Authenticate() for servers that just reply with - "+OK\r\n" to the SASL challenge. (issue #171) + "+OK\r\n" to the SASL challenge. (issue [#171](https://github.com/jstedfast/MailKit/issues/171)) * Clear the POP3 capability flags if the POP3 server responds with -ERR at any time. Some servers will reply with a list of capabilities until the client is authenticated, and then reply with -ERR meaning that the client - should not attempt to use previously listed capabilities. (issue #174) + should not attempt to use previously listed capabilities. (issue [#174](https://github.com/jstedfast/MailKit/issues/174)) -### MailKit 1.0.13 +## MailKit 1.0.13 (2015-03-29) * Added a FileName convenience property to BodyPartBasic which works the same way as the MimeKit.MimePart.FileName property. @@ -395,7 +1369,7 @@ * Added a work-around for Cyrus IMAP 2.4.16 sending untagged SEARCH responses when untagged ESEARCH responses are expected. -### MailKit 1.0.12 +## MailKit 1.0.12 (2015-03-21) * Fixed ImapFolder.GetMessage(), GetBodyPart() and GetStream() to throw an ImapCommandException rather than returning null if the server did not @@ -403,13 +1377,13 @@ * Added new, much more usable, Connect() methods to ImapClient, Pop3Client, and SmtpClient that take a hostname, port, and SecureSocketOptions. * Added a workaround for smtp.strato.de's blatant disregard for standards. - (issue #162) + (issue [#162](https://github.com/jstedfast/MailKit/issues/162)) * Fixed ImapFolder.Close() to require ReadWrite access if expunge is true. * Fixed IMAP SORT queries to inject "RETURN" before the orderBy param. - (issue #164) + (issue [#164](https://github.com/jstedfast/MailKit/issues/164)) * Implemented support for the IMAP ACL extension. -### MailKit 1.0.11 +## MailKit 1.0.11 (2015-03-14) * Make sure that the IMAP stream supports timeouts before using them (fixes a regression introduced in 1.0.10). @@ -422,11 +1396,11 @@ * Added an ImapClient.IsIdle property to check if the ImapClient is currently in the IDLE state. -### MailKit 1.0.10 +## MailKit 1.0.10 (2015-03-08) * Added support for the IMAP ID extension. -### MailKit 1.0.9 +## MailKit 1.0.9 (2015-03-02) * Modified UniqueId to contain a Validity value. This allows ImapFolder.Append(), CopyTo(), and MoveTo() to provide the caller with a way to make sure that the @@ -435,30 +1409,30 @@ only makes more sense but also simplifies comparison. * Fixed GMail Label APIs to use the modified UTF-7 encoding logic meant for folder names as it appears that GMail wants label names to be encoded in this - way. (issue #154) + way. (issue [#154](https://github.com/jstedfast/MailKit/issues/154)) -### MailKit 1.0.8 +## MailKit 1.0.8 (2015-02-19) -* Fixed the SMTP BINARYMIME extension support to work properly. (issue #151) +* Fixed the SMTP BINARYMIME extension support to work properly. (issue [#151](https://github.com/jstedfast/MailKit/issues/151)) * Fixed ImapFolder.Open() to not set the PermanentFlags to None if another folder was open (preventing SetFlags/AddFlags/RemoveFlags from functioning - properly). (issue #153) + properly). (issue [#153](https://github.com/jstedfast/MailKit/issues/153)) -### MailKit 1.0.7 +## MailKit 1.0.7 (2015-02-17) * Marked Pop3Client methods that take UIDs as [Obsolete]. It is suggested that the equivalent methods that take indexes be used instead and that UID-to-index mapping is done by the developer. This takes the burden off of the Pop3Client to maintain a mapping of UIDs to indexes that it cannot easily maintain. * Fixed SmtpCommandException to only serialize the Mailbox property when it is - non-null. (issue #148) + non-null. (issue [#148](https://github.com/jstedfast/MailKit/issues/148)) * Fixed IMAP support to accept a UIDVALIDITY value of 0 (even though it is - technically illegal) to work around a bug in SmarterMail 13.0. (issue #150) + technically illegal) to work around a bug in SmarterMail 13.0. (issue [#150](https://github.com/jstedfast/MailKit/issues/150)) * Fixed ImapFolder.GetSubfolders() to filter out non-child folders from the list that it returns (once again, a work-around for a SmarterMail 13.0 bug). - (issue #149) + (issue [#149](https://github.com/jstedfast/MailKit/issues/149)) -### MailKit 1.0.6 +## MailKit 1.0.6 (2015-01-18) * Fixed some issues revealed by source analysis. * Migrated the iOS assemblies to Xamarin.iOS Unified API for 64-bit support. @@ -467,31 +1441,31 @@ Note: If you are not yet ready to port your iOS application to the Unified API, you will need to stick with the 1.0.5 release. The Classic MonoTouch API is no longer supported. -### MailKit 1.0.5 +## MailKit 1.0.5 (2015-01-08) -* Added Connect() overloads which takes a Socket argument (issue #128). -* Added support for SMTP Delivery Status Notifications (issue #136). +* Added Connect() overloads which takes a Socket argument (issue [#128](https://github.com/jstedfast/MailKit/issues/128)). +* Added support for SMTP Delivery Status Notifications (issue [#136](https://github.com/jstedfast/MailKit/issues/136)). * Modified the ImapFolder logic such that if the IMAP server does not send a PERMANENTFLAGS resp-code when SELECTing the folder, then it - will assume that all flags are permanent (issue #140). + will assume that all flags are permanent (issue [#140](https://github.com/jstedfast/MailKit/issues/140)). -### MailKit 1.0.4 +## MailKit 1.0.4 (2014-12-13) * Modified the IMAP BODYSTRUCTURE parser to allow NIL tokens for - Content-Type and Content-Disposition parameter values. (issue #124) + Content-Type and Content-Disposition parameter values. (issue [#124](https://github.com/jstedfast/MailKit/issues/124)) * Added ImapFolder.GetBodyPart() overrides to allow fetching body parts - based on a part specifier string. (issue #130) + based on a part specifier string. (issue [#130](https://github.com/jstedfast/MailKit/issues/130)) -### MailKit 1.0.3 +## MailKit 1.0.3 (2014-12-05) -* Added a new ImapFolder.Fetch() overload that takes a HashSet - of header fields to fetch instead of a HashSet for +* Added a new ImapFolder.Fetch() overload that takes a HashSet<string> + of header fields to fetch instead of a HashSet<HeaderId> for developers that need the ability to request custom headers not defined in the HeaderId enum. * Added an SmtpClient.MessageSent event and an OnMessageSent() method that can be overridden. -### MailKit 1.0.2 +## MailKit 1.0.2 (2014-11-23) * Modified ProtocolLogger to flush the stream at the end of each Log(). * Fixed IMAP SEARCH queries with empty string arguments. @@ -499,13 +1473,12 @@ Note: If you are not yet ready to port your iOS application to the Unified API, header field names. * Improved documentation. -### MailKit 1.0.1 +## MailKit 1.0.1 (2014-10-27) * Fixed Pop3Client.GetMessages (int startIndex, int count, ...) to use 1-based sequence numbers. -* Fixed POP3 PIPELINING support to work as intended (issue #114). +* Fixed POP3 PIPELINING support to work as intended (issue [#114](https://github.com/jstedfast/MailKit/issues/114)). * Added a work-around for Office365.com IMAP to avoid ImapProtocolExceptions about unexpected '[' tokens when moving or - copying messages between folders (issue #115). -* Disabled SSLv3 for security reasons (POODLE), opting instead to use - TLS. + copying messages between folders (issue [#115](https://github.com/jstedfast/MailKit/issues/115)). +* Disabled SSLv3 for security reasons (POODLE), opting instead to use TLS. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000000..42e38c8bf3 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,31 @@ +# Security Policy + +The MailKit team takes the security of our software products and services seriously. + +If you believe you have found a security vulnerability in the MailKit repository, please report it to us as described below. + +## Supported Versions + +Due to the fact that the MailKit team is small (currently only myself), I can +only support security fixes for the latest version. + +| Version | Supported | +| ------- | ------------------ | +| 4.x | :white_check_mark: | +| < 4.0 | :x: | + +## Reporting a Vulnerability + +**Please do not report security vulnerabilities through public GitHub issues.** + +Instead, please report them via a [Private Security Advisory](https://github.com/jstedfast/MailKit/security/advisories/new) submission. + +For more information about the fields available and guidance on filling in the form, see +"[Creating a repository security advisory](https://docs.github.com/en/code-security/security-advisories/working-with-repository-security-advisories/creating-a-repository-security-advisory)" +and "[Best practices for writing repository security advisories.](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/best-practices-for-writing-repository-security-advisories)" + +You should receive a response within 24 hours. If for some reason you do not, please follow up via [email](mailto:jestedfa@microsoft.com?subject=MailKit%20Security%20Advisory) to ensure we received your original message. + +## Preferred Languages + +All communications should be in English. diff --git a/TODO.md b/TODO.md index 108cf5e611..1985225b9a 100644 --- a/TODO.md +++ b/TODO.md @@ -2,15 +2,6 @@ * SASL Authentication * Include code to fetch an OAuth2 token? - * ANONYMOUS - * GSSAPI -* SMTP Client - * CHUNKING (the BDAT command is already implemented and used by BINARYMIME but - perhaps the BDAT command could be used always when the server supports the - CHUNKING extension to avoid needing to byte-stuff the message?) - * Throw an exception if the MimeMessage is larger than the SIZE value? -* POP3 Client - * Rename Pop3Client.DeleteMessage() to Pop3Client.Delete()? Less verbose... * IMAP4 Client * Consolidate MessageFlagsChanged, MessageLabelsChanged, and ModSeqChanged events into a single event? * Extensions: @@ -18,8 +9,12 @@ * CATENATE * LIST-EXTENDED (Note: partially implemented already) * CONVERT (Note: none of the mainstream IMAP servers seem to support this) - * ANNOTATE - * NOTIFY (Note: only Dovecot seems to support this) * MULTISEARCH (Note: none of the mainstream IMAP servers seem to support this) + * UNAUTHENTICATE +* MessageThreader + * Fix UniqueId property to be just a UniqueId instead of Nullable. +* IMailFolder + * Modify Append() methods to simply return UniqueId instead of Nullable? + * Modify CopyTo/MoveTo() methods to also return UniqueId instead of Nullable? * Maildir -* Thunderbird mbox folder trees +* Thunderbird-style mbox folder trees? diff --git a/Telemetry.md b/Telemetry.md new file mode 100644 index 0000000000..259243fd64 --- /dev/null +++ b/Telemetry.md @@ -0,0 +1,384 @@ +# MailKit Telemetry Documentation + +## Socket Metrics + +### Metric: `mailkit.net.socket.connect.count` + +**Status:** [Experimental](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.30.0/specification/document-status.md) + +| **Name** | **Instrument Type** | **Unit** | **Description** | +|:----------------------------------------------|:--------------------|:----------------|:---------------------------------------------------------------------------| +| `mailkit.net.socket.connect.count` | Counter | `{attempt}` | The number of times a socket attempted to connect to a remote host. | + +| **Attribute** | **Type** | **Description** | **Examples** | **Presence** | +|:---------------------------|:---------|:-------------------------------------------------|:------------------------------------------------|:----------------------| +| `network.peer.address` | string | Peer IP address of the socket connection. | `142.251.167.109` | Always | +| `server.address` | string | The host name that the socket is connecting to. | `smtp.gmail.com` | Always | +| `server.port` | int | The port that the socket is connecting to. | `465` | Always | +| `error.type` | string | The type of error encountered. | `host_not_found`, `host_unreachable`, ... | If an error occurred. | + +This metric tracks the number of times a socket attempted to connect to a remote host. + +`error.type` has the following values: + +| **Value** | **Description** | +|:------------------------|:-------------------------------------------------------------------------------| +| `cancelled` | The operation was cancelled. | +| `host_not_found` | No such host is known. The name is not an official host name or alias. | +| `host_unreachable` | There is no network route to the specified host. | +| `network_unreachable` | No route to the remote host exists. | +| `connection_aborted` | The connection was aborted by .NET or the underlying socket provider. | +| `connection_refused` | The remote host is actively refusing a connection. | +| `connection_reset` | The connection was reset by the remote peer. | +| `timed_out` | The connection attempt timed out, or the connected host has failed to respond. | +| `too_many_open_sockets` | There are too many open sockets in the underlying socket provider. | + +Available starting in: MailKit v4.7.0 + +### Metric: `mailkit.net.socket.connect.duration` + +**Status:** [Experimental](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.30.0/specification/document-status.md) + +| **Name** | **Instrument Type** | **Unit** | **Description** | +|:----------------------------------------------|:--------------------|:----------------|:---------------------------------------------------------------------------| +| `mailkit.net.socket.connect.duration` | Histogram | `ms` | The number of milliseconds taken for a socket to connect to a remote host. | + +| **Attribute** | **Type** | **Description** | **Examples** | **Presence** | +|:---------------------------|:---------|:-------------------------------------------------|:------------------------------------------------|:----------------------| +| `network.peer.address` | string | Peer IP address of the socket connection. | `142.251.167.109` | Always | +| `server.address` | string | The host name that the socket is connecting to. | `smtp.gmail.com` | Always | +| `server.port` | int | The port that the socket is connecting to. | `465` | Always | +| `error.type` | string | The type of error encountered. | `host_not_found`, `host_unreachable`, ... | If an error occurred. | + +This metric measures the time it takes to connect a socket to a remote host. + +`error.type` has the following values: + +| **Value** | **Description** | +|:--------------------------|:----------------------------------------------------------------------------------------| +| `cancelled` | An operation was cancelled. | +| `host_not_found` | No such host is known. The name is not an official host name or alias. | +| `host_unreachable` | There is no network route to the specified host. | +| `network_unreachable` | No route to the remote host exists. | +| `connection_aborted` | The connection was aborted by .NET or the underlying socket provider. | +| `connection_refused` | The remote host is actively refusing a connection. | +| `connection_reset` | The connection was reset by the remote peer. | +| `timed_out` | The connection attempt timed out, or the connected host has failed to respond. | +| `too_many_open_sockets` | There are too many open sockets in the underlying socket provider. | + +Available starting in: MailKit v4.7.0 + +## SmtpClient Metrics + +### Metric: `mailkit.net.smtp.client.connection.duration` + +**Status:** [Experimental](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.30.0/specification/document-status.md) + +| **Name** | **Instrument Type** | **Unit** | **Description** | +|:----------------------------------------------|:--------------------|:----------------|:---------------------------------------------------------------------------| +| `mailkit.net.smtp.client.connection.duration` | Histogram | `s` | The duration of successfully established connections to an SMTP server. | + +| **Attribute** | **Type** | **Description** | **Examples** | **Presence** | +|:---------------------------|:---------|:-------------------------------------------------|:------------------------------------------------|:----------------------| +| `server.address` | string | The host name that the client is connected to. | `smtp.gmail.com` | Always | +| `server.port` | int | The port that the client is connected to. | `25`, `465`, `587` | Always | +| `url.scheme` | string | The URL scheme of the protocol used. | `smtp` or `smtps` | Always | +| `error.type` | string | The type of error encountered. | `host_not_found`, `host_unreachable`, ... | If an error occurred. | +| `network.operation` | string | The name of the operation. | `connect`, `authenticate`, `send`, ... | Always | + +This metric tracks the connection duration of each SmtpClient connection and records any error details if the connection was terminated involuntarily. + +`error.type` has the following values: + +| **Value** | **Description** | +|:--------------------------|:----------------------------------------------------------------------------------------| +| `cancelled` | An operation was cancelled. | +| `connection_aborted` | The connection was aborted by .NET or the underlying socket provider. | +| `connection_reset` | The connection was reset by the remote peer. | +| `timed_out` | The connection attempt timed out, or the connected host has failed to respond. | +| `secure_connection_error` | An SSL or TLS connection could not be negotiated. | +| `protocol_error` | The connection was terminated due to an incomplete or invalid response from the server. | + +Available starting in: MailKit v4.7.0 + +### Metric: `mailkit.net.smtp.client.operation.count` + +**Status:** [Experimental](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.30.0/specification/document-status.md) + +| **Name** | **Instrument Type** | **Unit** | **Description** | +|:----------------------------------------------|:--------------------|:----------------|:---------------------------------------------------------------------------| +| `mailkit.net.smtp.client.operation.count` | Counter | `{operation}` | The number of times a client performed an operation on an SMTP server. | + +| **Attribute** | **Type** | **Description** | **Examples** | **Presence** | +|:---------------------------|:---------|:-------------------------------------------------|:------------------------------------------------|:----------------------| +| `server.address` | string | The host name that the client is connected to. | `smtp.gmail.com` | Always | +| `server.port` | int | The port that the client is connected to. | `25`, `465`, `587` | Always | +| `url.scheme` | string | The URL scheme of the protocol used. | `smtp` or `smtps` | Always | +| `error.type` | string | The type of error encountered. | `host_not_found`, `host_unreachable`, ... | If an error occurred. | +| `network.operation` | string | The name of the operation. | `connect`, `authenticate`, `send`, ... | Always | + +This metric tracks the number of times an SmtpClient has performed an operation on an SMTP server. + +`error.type` has the following values: + +| **Value** | **Description** | +|:--------------------------|:----------------------------------------------------------------------------------------| +| `cancelled` | An operation was cancelled. | +| `host_not_found` | No such host is known. The name is not an official host name or alias. | +| `host_unreachable` | There is no network route to the specified host. | +| `network_unreachable` | No route to the remote host exists. | +| `connection_aborted` | The connection was aborted by .NET or the underlying socket provider. | +| `connection_refused` | The remote host is actively refusing a connection. | +| `connection_reset` | The connection was reset by the remote peer. | +| `timed_out` | The connection attempt timed out, or the connected host has failed to respond. | +| `too_many_open_sockets` | There are too many open sockets in the underlying socket provider. | +| `secure_connection_error` | An SSL or TLS connection could not be negotiated. | +| `protocol_error` | The connection was terminated due to an incomplete or invalid response from the server. | + +Available starting in: MailKit v4.7.0 + +### Metric: `mailkit.net.smtp.client.operation.duration` + +**Status:** [Experimental](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.30.0/specification/document-status.md) + +| **Name** | **Instrument Type** | **Unit** | **Description** | +|:----------------------------------------------|:--------------------|:----------------|:---------------------------------------------------------------------------| +| `mailkit.net.smtp.client.operation.duration` | Histogram | `ms` | The amount of time it takes for the SMTP server to perform an operation. | + +| **Attribute** | **Type** | **Description** | **Examples** | **Presence** | +|:---------------------------|:---------|:-------------------------------------------------|:------------------------------------------------|:----------------------| +| `server.address` | string | The host name that the client is connected to. | `smtp.gmail.com` | Always | +| `server.port` | int | The port that the client is connected to. | `25`, `465`, `587` | Always | +| `url.scheme` | string | The URL scheme of the protocol used. | `smtp` or `smtps` | Always | +| `error.type` | string | The type of error encountered. | `host_not_found`, `host_unreachable`, ... | If an error occurred. | +| `network.operation` | string | The name of the operation. | `connect`, `authenticate`, `send`, ... | Always | + +This metric tracks the amount of time it takes an SMTP server to perform an operation. + +`error.type` has the following values: + +| **Value** | **Description** | +|:--------------------------|:----------------------------------------------------------------------------------------| +| `cancelled` | An operation was cancelled. | +| `host_not_found` | No such host is known. The name is not an official host name or alias. | +| `host_unreachable` | There is no network route to the specified host. | +| `network_unreachable` | No route to the remote host exists. | +| `connection_aborted` | The connection was aborted by .NET or the underlying socket provider. | +| `connection_refused` | The remote host is actively refusing a connection. | +| `connection_reset` | The connection was reset by the remote peer. | +| `timed_out` | The connection attempt timed out, or the connected host has failed to respond. | +| `too_many_open_sockets` | There are too many open sockets in the underlying socket provider. | +| `secure_connection_error` | An SSL or TLS connection could not be negotiated. | +| `protocol_error` | The connection was terminated due to an incomplete or invalid response from the server. | + +Available starting in: MailKit v4.7.0 + +## Pop3Client Metrics + +### Metric: `mailkit.net.pop3.client.connection.duration` + +**Status:** [Experimental](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.30.0/specification/document-status.md) + +| **Name** | **Instrument Type** | **Unit** | **Description** | +|:----------------------------------------------|:--------------------|:----------------|:---------------------------------------------------------------------------| +| `mailkit.net.pop3.client.connection.duration` | Histogram | `s` | The duration of successfully established connections to a POP3 server. | + +| **Attribute** | **Type** | **Description** | **Examples** | **Presence** | +|:---------------------------|:---------|:-------------------------------------------------|:------------------------------------------------|:----------------------| +| `server.address` | string | The host name that the client is connected to. | `pop.gmail.com` | Always | +| `server.port` | int | The port that the client is connected to. | `110`, `995` | Always | +| `url.scheme` | string | The URL scheme of the protocol used. | `pop3` or `pop3s` | Always | +| `error.type` | string | The type of error encountered. | `host_not_found`, `host_unreachable`, ... | If an error occurred. | +| `network.operation` | string | The name of the operation. | `connect`, `authenticate`, ... | Always | + +This metric tracks the connection duration of each Pop3Client connection and records any error details if the connection was terminated involuntarily. + +`error.type` has the following values: + +| **Value** | **Description** | +|:--------------------------|:----------------------------------------------------------------------------------------| +| `cancelled` | An operation was cancelled. | +| `connection_aborted` | The connection was aborted by .NET or the underlying socket provider. | +| `connection_reset` | The connection was reset by the remote peer. | +| `timed_out` | The connection attempt timed out, or the connected host has failed to respond. | +| `secure_connection_error` | An SSL or TLS connection could not be negotiated. | +| `protocol_error` | The connection was terminated due to an incomplete or invalid response from the server. | + +Available starting in: MailKit v4.7.0 + +### Metric: `mailkit.net.pop3.client.operation.count` + +**Status:** [Experimental](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.30.0/specification/document-status.md) + +| **Name** | **Instrument Type** | **Unit** | **Description** | +|:----------------------------------------------|:--------------------|:----------------|:---------------------------------------------------------------------------| +| `mailkit.net.pop3.client.operation.count` | Counter | `{operation}` | The number of times a client performed an operation on a POP3 server. | + +| **Attribute** | **Type** | **Description** | **Examples** | **Presence** | +|:---------------------------|:---------|:-------------------------------------------------|:------------------------------------------------|:----------------------| +| `server.address` | string | The host name that the client is connected to. | `pop.gmail.com` | Always | +| `server.port` | int | The port that the client is connected to. | `110`, `995` | Always | +| `url.scheme` | string | The URL scheme of the protocol used. | `pop3` or `pop3s` | Always | +| `error.type` | string | The type of error encountered. | `host_not_found`, `host_unreachable`, ... | If an error occurred. | +| `network.operation` | string | The name of the operation. | `connect`, `authenticate`, ... | Always | + +This metric tracks the number of times an Pop3Client has performed an operation on a POP3 server. + +`error.type` has the following values: + +| **Value** | **Description** | +|:--------------------------|:----------------------------------------------------------------------------------------| +| `cancelled` | An operation was cancelled. | +| `host_not_found` | No such host is known. The name is not an official host name or alias. | +| `host_unreachable` | There is no network route to the specified host. | +| `network_unreachable` | No route to the remote host exists. | +| `connection_aborted` | The connection was aborted by .NET or the underlying socket provider. | +| `connection_refused` | The remote host is actively refusing a connection. | +| `connection_reset` | The connection was reset by the remote peer. | +| `timed_out` | The connection attempt timed out, or the connected host has failed to respond. | +| `too_many_open_sockets` | There are too many open sockets in the underlying socket provider. | +| `secure_connection_error` | An SSL or TLS connection could not be negotiated. | +| `protocol_error` | The connection was terminated due to an incomplete or invalid response from the server. | + +Available starting in: MailKit v4.7.0 + +### Metric: `mailkit.net.pop3.client.operation.duration` + +**Status:** [Experimental](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.30.0/specification/document-status.md) + +| **Name** | **Instrument Type** | **Unit** | **Description** | +|:----------------------------------------------|:--------------------|:----------------|:---------------------------------------------------------------------------| +| `mailkit.net.pop3.client.operation.duration` | Histogram | `ms` | The amount of time it takes for the POP3 server to perform an operation. | + +| **Attribute** | **Type** | **Description** | **Examples** | **Presence** | +|:---------------------------|:---------|:-------------------------------------------------|:------------------------------------------------|:----------------------| +| `server.address` | string | The host name that the client is connected to. | `pop.gmail.com` | Always | +| `server.port` | int | The port that the client is connected to. | `110`, `995` | Always | +| `url.scheme` | string | The URL scheme of the protocol used. | `pop3` or `pop3s` | Always | +| `error.type` | string | The type of error encountered. | `host_not_found`, `host_unreachable`, ... | If an error occurred. | +| `network.operation` | string | The name of the operation. | `connect`, `authenticate`, ... | Always | + +This metric tracks the amount of time it takes a POP3 server to perform an operation. + +`error.type` has the following values: + +| **Value** | **Description** | +|:--------------------------|:----------------------------------------------------------------------------------------| +| `cancelled` | An operation was cancelled. | +| `host_not_found` | No such host is known. The name is not an official host name or alias. | +| `host_unreachable` | There is no network route to the specified host. | +| `network_unreachable` | No route to the remote host exists. | +| `connection_aborted` | The connection was aborted by .NET or the underlying socket provider. | +| `connection_refused` | The remote host is actively refusing a connection. | +| `connection_reset` | The connection was reset by the remote peer. | +| `timed_out` | The connection attempt timed out, or the connected host has failed to respond. | +| `too_many_open_sockets` | There are too many open sockets in the underlying socket provider. | +| `secure_connection_error` | An SSL or TLS connection could not be negotiated. | +| `protocol_error` | The connection was terminated due to an incomplete or invalid response from the server. | + +Available starting in: MailKit v4.7.0 + +## ImapClient Metrics + +### Metric: `mailkit.net.imap.client.connection.duration` + +**Status:** [Experimental](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.30.0/specification/document-status.md) + +| **Name** | **Instrument Type** | **Unit** | **Description** | +|:----------------------------------------------|:--------------------|:----------------|:---------------------------------------------------------------------------| +| `mailkit.net.imap.client.connection.duration` | Histogram | `s` | The duration of successfully established connections to an IMAP server. | + +| **Attribute** | **Type** | **Description** | **Examples** | **Presence** | +|:---------------------------|:---------|:-------------------------------------------------|:------------------------------------------------|:----------------------| +| `server.address` | string | The host name that the client is connected to. | `imap.gmail.com` | Always | +| `server.port` | int | The port that the client is connected to. | `143`, `993` | Always | +| `url.scheme` | string | The URL scheme of the protocol used. | `imap` or `imaps` | Always | +| `error.type` | string | The type of error encountered. | `host_not_found`, `host_unreachable`, ... | If an error occurred. | +| `network.operation` | string | The name of the operation. | `connect`, `authenticate`, ... | Always | + +This metric tracks the connection duration of each ImapClient connection and records any error details if the connection was terminated involuntarily. + +`error.type` has the following values: + +| **Value** | **Description** | +|:--------------------------|:----------------------------------------------------------------------------------------| +| `cancelled` | An operation was cancelled. | +| `connection_aborted` | The connection was aborted by .NET or the underlying socket provider. | +| `connection_reset` | The connection was reset by the remote peer. | +| `timed_out` | The connection attempt timed out, or the connected host has failed to respond. | +| `secure_connection_error` | An SSL or TLS connection could not be negotiated. | +| `protocol_error` | The connection was terminated due to an incomplete or invalid response from the server. | + +Available starting in: MailKit v4.7.0 + +### Metric: `mailkit.net.imap.client.operation.count` + +**Status:** [Experimental](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.30.0/specification/document-status.md) + +| **Name** | **Instrument Type** | **Unit** | **Description** | +|:----------------------------------------------|:--------------------|:----------------|:---------------------------------------------------------------------------| +| `mailkit.net.imap.client.operation.count` | Counter | `{operation}` | The number of times a client performed an operation on an IMAP server. | + +| **Attribute** | **Type** | **Description** | **Examples** | **Presence** | +|:---------------------------|:---------|:-------------------------------------------------|:------------------------------------------------|:----------------------| +| `server.address` | string | The host name that the client is connected to. | `imap.gmail.com` | Always | +| `server.port` | int | The port that the client is connected to. | `143`, `993` | Always | +| `url.scheme` | string | The URL scheme of the protocol used. | `imap` or `imaps` | Always | +| `error.type` | string | The type of error encountered. | `host_not_found`, `host_unreachable`, ... | If an error occurred. | +| `network.operation` | string | The name of the operation. | `connect`, `authenticate`, ... | Always | + +This metric tracks the number of times an ImapClient has performed an operation on an IMAP server. + +`error.type` has the following values: + +| **Value** | **Description** | +|:--------------------------|:----------------------------------------------------------------------------------------| +| `cancelled` | An operation was cancelled. | +| `host_not_found` | No such host is known. The name is not an official host name or alias. | +| `host_unreachable` | There is no network route to the specified host. | +| `network_unreachable` | No route to the remote host exists. | +| `connection_aborted` | The connection was aborted by .NET or the underlying socket provider. | +| `connection_refused` | The remote host is actively refusing a connection. | +| `connection_reset` | The connection was reset by the remote peer. | +| `timed_out` | The connection attempt timed out, or the connected host has failed to respond. | +| `too_many_open_sockets` | There are too many open sockets in the underlying socket provider. | +| `secure_connection_error` | An SSL or TLS connection could not be negotiated. | +| `protocol_error` | The connection was terminated due to an incomplete or invalid response from the server. | + +Available starting in: MailKit v4.7.0 + +### Metric: `mailkit.net.imap.client.operation.duration` + +**Status:** [Experimental](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.30.0/specification/document-status.md) + +| **Name** | **Instrument Type** | **Unit** | **Description** | +|:----------------------------------------------|:--------------------|:----------------|:---------------------------------------------------------------------------| +| `mailkit.net.imap.client.operation.duration` | Histogram | `ms` | The amount of time it takes for the IMAP server to perform an operation. | + +| **Attribute** | **Type** | **Description** | **Examples** | **Presence** | +|:---------------------------|:---------|:-------------------------------------------------|:------------------------------------------------|:----------------------| +| `server.address` | string | The host name that the client is connected to. | `imap.gmail.com` | Always | +| `server.port` | int | The port that the client is connected to. | `143`, `993` | Always | +| `url.scheme` | string | The URL scheme of the protocol used. | `imap` or `imaps` | Always | +| `error.type` | string | The type of error encountered. | `host_not_found`, `host_unreachable`, ... | If an error occurred. | +| `network.operation` | string | The name of the operation. | `connect`, `authenticate`, ... | Always | + +This metric tracks the amount of time it takes an IMAP server to perform an operation. + +`error.type` has the following values: + +| **Value** | **Description** | +|:--------------------------|:----------------------------------------------------------------------------------------| +| `cancelled` | An operation was cancelled. | +| `host_not_found` | No such host is known. The name is not an official host name or alias. | +| `host_unreachable` | There is no network route to the specified host. | +| `network_unreachable` | No route to the remote host exists. | +| `connection_aborted` | The connection was aborted by .NET or the underlying socket provider. | +| `connection_refused` | The remote host is actively refusing a connection. | +| `connection_reset` | The connection was reset by the remote peer. | +| `timed_out` | The connection attempt timed out, or the connected host has failed to respond. | +| `too_many_open_sockets` | There are too many open sockets in the underlying socket provider. | +| `secure_connection_error` | An SSL or TLS connection could not be negotiated. | +| `protocol_error` | The connection was terminated due to an incomplete or invalid response from the server. | + +Available starting in: MailKit v4.7.0 diff --git a/UnitTests/AccessControlListTests.cs b/UnitTests/AccessControlListTests.cs new file mode 100644 index 0000000000..ecdc4e0666 --- /dev/null +++ b/UnitTests/AccessControlListTests.cs @@ -0,0 +1,163 @@ +// +// AccessControlListTests.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System.Collections; + +using MailKit; + +namespace UnitTests { + [TestFixture] + public class AccessControlListTests + { + [Test] + public void TestArgumentExceptions () + { + var enumeratedRights = new [] { AccessRight.OpenFolder, AccessRight.CreateFolder }; + var array = new AccessRight[10]; + + var rights = new AccessRights (enumeratedRights); + Assert.Throws (() => rights.AddRange ((string) null)); + Assert.Throws (() => rights.AddRange ((IEnumerable) null)); + Assert.Throws (() => new AccessRights ((string) null)); + Assert.Throws (() => new AccessRights ((IEnumerable) null)); + Assert.Throws (() => { var x = rights [-1]; }); + Assert.Throws (() => rights.CopyTo (null, 0)); + Assert.Throws (() => rights.CopyTo (array, -1)); + + var control = new AccessControl ("control"); + Assert.Throws (() => new AccessControl (null)); + Assert.Throws (() => new AccessControl (null, "rk")); + Assert.Throws (() => new AccessControl (null, enumeratedRights)); + Assert.Throws (() => new AccessControl ("name", (string) null)); + Assert.Throws (() => new AccessControl ("name", (IEnumerable) null)); + + var list = new AccessControlList (); + Assert.Throws (() => new AccessControlList (null)); + //Assert.Throws (() => list.Add (null)); + Assert.Throws (() => list.AddRange (null)); + } + + [Test] + public void TestAccessRight () + { + Assert.That (AccessRight.Administer == new AccessRight (AccessRight.Administer.Right), Is.True, "=="); + Assert.That (AccessRight.Administer == new AccessRight (AccessRight.OpenFolder.Right), Is.False, "=="); + + Assert.That (AccessRight.Administer != new AccessRight (AccessRight.Administer.Right), Is.False, "!="); + Assert.That (AccessRight.Administer != new AccessRight (AccessRight.OpenFolder.Right), Is.True, "!="); + + Assert.That (AccessRight.Administer.Equals ((object) new AccessRight (AccessRight.Administer.Right)), Is.True, "Equals"); + Assert.That (new AccessRight (AccessRight.Administer.Right).GetHashCode (), Is.EqualTo (AccessRight.Administer.GetHashCode ()), "GetHashCode"); + + Assert.That (AccessRight.Administer.ToString (), Is.EqualTo ("a"), "ToString"); + } + + [Test] + public void TestAccessRights () + { + var expected = new [] { AccessRight.OpenFolder, AccessRight.CreateFolder, AccessRight.DeleteFolder, AccessRight.ExpungeFolder, AccessRight.AppendMessages, AccessRight.SetMessageDeleted }; + var rights = new AccessRights (); + int i; + + Assert.That (rights.IsReadOnly, Is.False, "IsReadOnly"); + + Assert.That (rights.Add (AccessRight.OpenFolder), Is.True, "Add OpenFolder"); + Assert.That (rights, Has.Count.EqualTo (1), "Count after adding OpenFolder"); + Assert.That (rights.Add (AccessRight.OpenFolder), Is.False, "Add OpenFolder again"); + Assert.That (rights, Has.Count.EqualTo (1), "Count after adding OpenFolder again"); + + Assert.That (rights.Add (AccessRight.CreateFolder.Right), Is.True, "Add CreateFolder"); + Assert.That (rights, Has.Count.EqualTo (2), "Count after adding CreateFolder"); + Assert.That (rights.Add (AccessRight.CreateFolder), Is.False, "Add CreateFolder again"); + Assert.That (rights, Has.Count.EqualTo (2), "Count after adding OpenFolder again"); + + rights.AddRange (new [] { AccessRight.DeleteFolder, AccessRight.ExpungeFolder }); + Assert.That (rights, Has.Count.EqualTo (4), "Count after adding DeleteFolder and ExpungeFolder"); + + Assert.That (rights, Does.Contain (AccessRight.DeleteFolder), "Contains DeleteFolder"); + Assert.That (rights, Does.Contain (AccessRight.ExpungeFolder), "Contains ExpungeFolder"); + Assert.That (rights, Does.Not.Contain (AccessRight.Administer), "Contains Administer"); + + rights.AddRange ("it"); + Assert.That (rights, Has.Count.EqualTo (6), "Count after adding AppendMessages and SetMessageDeleted"); + + Assert.That (rights, Does.Contain (AccessRight.AppendMessages), "Contains AppendMessages"); + Assert.That (rights, Does.Contain (AccessRight.SetMessageDeleted), "Contains SetMessageDeleted"); + Assert.That (rights, Does.Not.Contain (AccessRight.Administer), "Contains Administer"); + + for (i = 0; i < 6; i++) + Assert.That (rights[i], Is.EqualTo (expected[i]), $"rights[{i}]"); + + ((ICollection) rights).Add (AccessRight.Administer); + Assert.That (rights.Remove (AccessRight.Administer), Is.True, "Remove Administer"); + Assert.That (rights.Remove (AccessRight.Administer), Is.False, "Remove Administer again"); + + i = 0; + foreach (var right in rights) + Assert.That (right, Is.EqualTo (expected[i]), $"foreach rights[{i++}]"); + + i = 0; + foreach (AccessRight right in ((IEnumerable) rights)) + Assert.That (right, Is.EqualTo (expected[i]), $"generic foreach rights[{i++}]"); + + var array = new AccessRight[rights.Count]; + rights.CopyTo (array, 0); + + for (i = 0; i < 6; i++) + Assert.That (array[i], Is.EqualTo (expected[i]), $"CopyTo[{i}]"); + + Assert.That (rights.ToString (), Is.EqualTo ("rkxeit"), "ToString"); + } + + [Test] + public void TestAccessControl () + { + var control = new AccessControl ("empty"); + + Assert.That (control.Name, Is.EqualTo ("empty"), "Name"); + Assert.That (control.Rights.ToString (), Is.EqualTo (""), "Rights (empty)"); + + control = new AccessControl ("admin", "a"); + + Assert.That (control.Name, Is.EqualTo ("admin"), "Name"); + Assert.That (control.Rights.ToString (), Is.EqualTo ("a"), "Rights (admin)"); + + control = new AccessControl ("it", new [] { AccessRight.AppendMessages, AccessRight.SetMessageDeleted }); + + Assert.That (control.Name, Is.EqualTo ("it"), "Name"); + Assert.That (control.Rights.ToString (), Is.EqualTo ("it"), "Rights (it)"); + } + + [Test] + public void TestAccessControlList () + { + var list = new AccessControlList (new [] { new AccessControl ("admin", new [] { AccessRight.Administer }) }); + + Assert.That (list, Has.Count.EqualTo (1), "Count"); + Assert.That (list[0].Name, Is.EqualTo ("admin"), "list[0].Name"); + } + } +} diff --git a/UnitTests/AnnotationAttributeTests.cs b/UnitTests/AnnotationAttributeTests.cs new file mode 100644 index 0000000000..4fc7cb932e --- /dev/null +++ b/UnitTests/AnnotationAttributeTests.cs @@ -0,0 +1,81 @@ +// +// AnnotationAttributeTests.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using MailKit; + +namespace UnitTests { + [TestFixture] + public class AnnotationAttributeTests + { + [Test] + public void TestArgumentExceptions () + { + Assert.Throws (() => new AnnotationAttribute (null)); + Assert.Throws (() => new AnnotationAttribute (string.Empty)); + Assert.Throws (() => new AnnotationAttribute ("*")); + Assert.Throws (() => new AnnotationAttribute ("%")); + Assert.Throws (() => new AnnotationAttribute ("w*ldcard")); + Assert.Throws (() => new AnnotationAttribute ("w%ldcard")); + } + + [Test] + public void TestBasicFunctionality () + { + AnnotationAttribute attr; + + attr = new AnnotationAttribute ("value"); + Assert.That (attr.Name, Is.EqualTo ("value"), "Name"); + Assert.That (attr.Specifier, Is.EqualTo ("value"), "Specifier"); + Assert.That (attr.Scope, Is.EqualTo (AnnotationScope.Both), "Scope"); + + attr = new AnnotationAttribute ("value.priv"); + Assert.That (attr.Name, Is.EqualTo ("value"), "Name"); + Assert.That (attr.Specifier, Is.EqualTo ("value.priv"), "Specifier"); + Assert.That (attr.Scope, Is.EqualTo (AnnotationScope.Private), "Scope"); + + attr = new AnnotationAttribute ("value.shared"); + Assert.That (attr.Name, Is.EqualTo ("value"), "Name"); + Assert.That (attr.Specifier, Is.EqualTo ("value.shared"), "Specifier"); + Assert.That (attr.Scope, Is.EqualTo (AnnotationScope.Shared), "Scope"); + } + + [Test] + public void TestEquality () + { + var value = new AnnotationAttribute ("value"); + + Assert.That (value, Is.EqualTo (AnnotationAttribute.Value), "AreEqual"); + Assert.That (AnnotationAttribute.Value.Equals (value), Is.True, ".Equals"); + Assert.That (value == AnnotationAttribute.Value, Is.True, "value == value"); + Assert.That (AnnotationAttribute.PrivateValue != AnnotationAttribute.SharedValue, Is.True, "value.priv != value.shared"); + + Assert.That (AnnotationAttribute.Value.Equals ((object) null), Is.False, "value.Equals ((object) null)"); + Assert.That (AnnotationAttribute.Value.Equals ((AnnotationAttribute) null), Is.False, "value.Equals ((AnnotationAttribute) null)"); + Assert.That (AnnotationAttribute.Value == null, Is.False, "value == null"); + Assert.That (AnnotationAttribute.Value != null, Is.True, "/comment != null"); + } + } +} diff --git a/UnitTests/AnnotationEntryTests.cs b/UnitTests/AnnotationEntryTests.cs new file mode 100644 index 0000000000..bd4917fc6c --- /dev/null +++ b/UnitTests/AnnotationEntryTests.cs @@ -0,0 +1,321 @@ +// +// AnnotationEntryTests.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using MimeKit; +using MailKit; + +namespace UnitTests { + [TestFixture] + public class AnnotationEntryTests + { + [Test] + public void TestArgumentExceptions () + { + Assert.Throws (() => new AnnotationEntry (null)); + Assert.Throws (() => new AnnotationEntry (string.Empty)); + Assert.Throws (() => new AnnotationEntry ("x")); // paths must begin with '/' + Assert.Throws (() => new AnnotationEntry ("/1.2.3.4.5")); // paths must not begin with a part-spec + Assert.Throws (() => new AnnotationEntry ("/台北/日本語")); // paths may not contain non-ascii characters + Assert.Throws (() => new AnnotationEntry ("/path/0wnz")); // path components must not begin with a number + Assert.Throws (() => new AnnotationEntry ("/root//node")); // path components must not contain "//" + Assert.Throws (() => new AnnotationEntry ("/root/")); // path components must not end with '/' + Assert.Throws (() => new AnnotationEntry ("/root..node")); // path components must not contain ".." + Assert.Throws (() => new AnnotationEntry ("/root./node")); // path components must not end with '.' + Assert.Throws (() => new AnnotationEntry ("/root.")); // path components must not end with '.' + + Assert.Throws (() => new AnnotationEntry ((string) null, "/comment")); + Assert.Throws (() => new AnnotationEntry ("1", null)); + Assert.Throws (() => new AnnotationEntry ("abc", "/comment")); // invalid part-spec + Assert.Throws (() => new AnnotationEntry ("1.", "/comment")); // invalid part-spec + Assert.Throws (() => new AnnotationEntry ("1..", "/comment")); // invalid part-spec + Assert.Throws (() => new AnnotationEntry ("1..2", "/comment")); // invalid part-spec + + Assert.Throws (() => new AnnotationEntry ((BodyPart) null, "/comment")); + + Assert.Throws (() => AnnotationEntry.Parse (null)); + } + + [Test] + public void TestBasicFunctionality () + { + var body = new BodyPartBasic (new ContentType ("image", "jpeg"), "1.2.3.4"); + AnnotationEntry entry; + + entry = new AnnotationEntry ("/comment"); + Assert.That (entry.Entry, Is.EqualTo ("/comment"), "Entry"); + Assert.That (entry.PartSpecifier, Is.Null, "PartSpecifier"); + Assert.That (entry.Path, Is.EqualTo ("/comment"), "Path"); + Assert.That (entry.Scope, Is.EqualTo (AnnotationScope.Both), "Scope"); + + entry = new AnnotationEntry ("/comment", AnnotationScope.Private); + Assert.That (entry.Entry, Is.EqualTo ("/comment.priv"), "Entry"); + Assert.That (entry.PartSpecifier, Is.Null, "PartSpecifier"); + Assert.That (entry.Path, Is.EqualTo ("/comment"), "Path"); + Assert.That (entry.Scope, Is.EqualTo (AnnotationScope.Private), "Scope"); + + entry = new AnnotationEntry ("/comment", AnnotationScope.Shared); + Assert.That (entry.Entry, Is.EqualTo ("/comment.shared"), "Entry"); + Assert.That (entry.PartSpecifier, Is.Null, "PartSpecifier"); + Assert.That (entry.Path, Is.EqualTo ("/comment"), "Path"); + Assert.That (entry.Scope, Is.EqualTo (AnnotationScope.Shared), "Scope"); + + + entry = new AnnotationEntry ("1.2.3.4", "/comment"); + Assert.That (entry.Entry, Is.EqualTo ("/1.2.3.4/comment"), "Entry"); + Assert.That (entry.PartSpecifier, Is.EqualTo ("1.2.3.4"), "PartSpecifier"); + Assert.That (entry.Path, Is.EqualTo ("/comment"), "Path"); + Assert.That (entry.Scope, Is.EqualTo (AnnotationScope.Both), "Scope"); + + entry = new AnnotationEntry ("1.2.3.4", "/comment", AnnotationScope.Private); + Assert.That (entry.Entry, Is.EqualTo ("/1.2.3.4/comment.priv"), "Entry"); + Assert.That (entry.PartSpecifier, Is.EqualTo ("1.2.3.4"), "PartSpecifier"); + Assert.That (entry.Path, Is.EqualTo ("/comment"), "Path"); + Assert.That (entry.Scope, Is.EqualTo (AnnotationScope.Private), "Scope"); + + entry = new AnnotationEntry ("1.2.3.4", "/comment", AnnotationScope.Shared); + Assert.That (entry.Entry, Is.EqualTo ("/1.2.3.4/comment.shared"), "Entry"); + Assert.That (entry.PartSpecifier, Is.EqualTo ("1.2.3.4"), "PartSpecifier"); + Assert.That (entry.Path, Is.EqualTo ("/comment"), "Path"); + Assert.That (entry.Scope, Is.EqualTo (AnnotationScope.Shared), "Scope"); + + + entry = new AnnotationEntry (body, "/comment"); + Assert.That (entry.Entry, Is.EqualTo ("/1.2.3.4/comment"), "Entry"); + Assert.That (entry.PartSpecifier, Is.EqualTo ("1.2.3.4"), "PartSpecifier"); + Assert.That (entry.Path, Is.EqualTo ("/comment"), "Path"); + Assert.That (entry.Scope, Is.EqualTo (AnnotationScope.Both), "Scope"); + + entry = new AnnotationEntry (body, "/comment", AnnotationScope.Private); + Assert.That (entry.Entry, Is.EqualTo ("/1.2.3.4/comment.priv"), "Entry"); + Assert.That (entry.PartSpecifier, Is.EqualTo ("1.2.3.4"), "PartSpecifier"); + Assert.That (entry.Path, Is.EqualTo ("/comment"), "Path"); + Assert.That (entry.Scope, Is.EqualTo (AnnotationScope.Private), "Scope"); + + entry = new AnnotationEntry (body, "/comment", AnnotationScope.Shared); + Assert.That (entry.Entry, Is.EqualTo ("/1.2.3.4/comment.shared"), "Entry"); + Assert.That (entry.PartSpecifier, Is.EqualTo ("1.2.3.4"), "PartSpecifier"); + Assert.That (entry.Path, Is.EqualTo ("/comment"), "Path"); + Assert.That (entry.Scope, Is.EqualTo (AnnotationScope.Shared), "Scope"); + } + + [Test] + public void TestEquality () + { + var comment = new AnnotationEntry ("/comment"); + + Assert.That (comment, Is.EqualTo (AnnotationEntry.Comment), "AreEqual"); + Assert.That (AnnotationEntry.Comment.Equals (comment), Is.True, ".Equals"); + Assert.That (comment == AnnotationEntry.Comment, Is.True, "/comment == /comment"); + Assert.That (AnnotationEntry.PrivateComment != AnnotationEntry.SharedComment, Is.True, "/comment.priv != /comment.shared"); + + Assert.That (AnnotationEntry.Comment.Equals ((object) null), Is.False, "/comment.Equals ((object) null)"); + Assert.That (AnnotationEntry.Comment.Equals ((AnnotationEntry) null), Is.False, "/comment.Equals ((AnnotationEntry) null)"); + Assert.That (AnnotationEntry.Comment == null, Is.False, "/comment == null"); + Assert.That (AnnotationEntry.Comment != null, Is.True, "/comment != null"); + } + + [Test] + public void TestParse () + { + AnnotationEntry entry; + + Assert.Throws (() => AnnotationEntry.Parse (string.Empty), "string.Empty"); + + // invalid part-specs + Assert.Throws (() => AnnotationEntry.Parse ("/1./comment"), "/1./comment"); + Assert.Throws (() => AnnotationEntry.Parse ("/1../comment"), "/1../comment"); + Assert.Throws (() => AnnotationEntry.Parse ("/1..2/comment"), "/1..2/comment"); + + // invalid paths + Assert.Throws (() => AnnotationEntry.Parse ("x"), "x"); // paths must begin with '/' + Assert.Throws (() => AnnotationEntry.Parse ("/1a/comment"), "/1a/comment"); // invalid character in part-spec + Assert.Throws (() => AnnotationEntry.Parse ("/1.2.3.4.5"), "/1.2.3.4.5"); // paths must not contain only a part-spec + Assert.Throws (() => AnnotationEntry.Parse ("/台北/日本語"), "/台北/日本語"); // paths may not contain non-ascii characters + Assert.Throws (() => AnnotationEntry.Parse ("/path/0wnz"), "/path/0wnz"); // path components must not begin with a number + Assert.Throws (() => AnnotationEntry.Parse ("/root//node"), "/root//node"); // path components must not contain "//" + Assert.Throws (() => AnnotationEntry.Parse ("/root/"), "/root/"); // path components must not end with '/' + Assert.Throws (() => AnnotationEntry.Parse ("/root..node"), "/root..node"); // path components must not contain ".." + Assert.Throws (() => AnnotationEntry.Parse ("/root./node"), "/root./node"); // path components must not end with '.' + Assert.Throws (() => AnnotationEntry.Parse ("/root."), "/root."); // path components must not end with '.' + + try { + entry = AnnotationEntry.Parse ("/comment"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect: {ex}"); + return; + } + Assert.That (entry.Entry, Is.EqualTo ("/comment"), "Entry"); + Assert.That (entry.PartSpecifier, Is.Null, "PartSpecifier"); + Assert.That (entry.Path, Is.EqualTo ("/comment"), "Path"); + Assert.That (entry.Scope, Is.EqualTo (AnnotationScope.Both), "Scope"); + + try { + entry = AnnotationEntry.Parse ("/comment.priv"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect: {ex}"); + return; + } + Assert.That (entry.Entry, Is.EqualTo ("/comment.priv"), "Entry"); + Assert.That (entry.PartSpecifier, Is.Null, "PartSpecifier"); + Assert.That (entry.Path, Is.EqualTo ("/comment"), "Path"); + Assert.That (entry.Scope, Is.EqualTo (AnnotationScope.Private), "Scope"); + + try { + entry = AnnotationEntry.Parse ("/comment.shared"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect: {ex}"); + return; + } + entry = new AnnotationEntry ("/comment", AnnotationScope.Shared); + Assert.That (entry.Entry, Is.EqualTo ("/comment.shared"), "Entry"); + Assert.That (entry.PartSpecifier, Is.Null, "PartSpecifier"); + Assert.That (entry.Path, Is.EqualTo ("/comment"), "Path"); + Assert.That (entry.Scope, Is.EqualTo (AnnotationScope.Shared), "Scope"); + + try { + entry = AnnotationEntry.Parse ("/1.2.3.4/comment"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect: {ex}"); + return; + } + Assert.That (entry.Entry, Is.EqualTo ("/1.2.3.4/comment"), "Entry"); + Assert.That (entry.PartSpecifier, Is.EqualTo ("1.2.3.4"), "PartSpecifier"); + Assert.That (entry.Path, Is.EqualTo ("/comment"), "Path"); + Assert.That (entry.Scope, Is.EqualTo (AnnotationScope.Both), "Scope"); + + try { + entry = AnnotationEntry.Parse ("/1.2.3.4/comment.priv"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect: {ex}"); + return; + } + Assert.That (entry.Entry, Is.EqualTo ("/1.2.3.4/comment.priv"), "Entry"); + Assert.That (entry.PartSpecifier, Is.EqualTo ("1.2.3.4"), "PartSpecifier"); + Assert.That (entry.Path, Is.EqualTo ("/comment"), "Path"); + Assert.That (entry.Scope, Is.EqualTo (AnnotationScope.Private), "Scope"); + + try { + entry = AnnotationEntry.Parse ("/1.2.3.4/comment.shared"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect: {ex}"); + return; + } + Assert.That (entry.Entry, Is.EqualTo ("/1.2.3.4/comment.shared"), "Entry"); + Assert.That (entry.PartSpecifier, Is.EqualTo ("1.2.3.4"), "PartSpecifier"); + Assert.That (entry.Path, Is.EqualTo ("/comment"), "Path"); + Assert.That (entry.Scope, Is.EqualTo (AnnotationScope.Shared), "Scope"); + } + + [Test] + public void TestCreate () + { + AnnotationEntry entry; + + try { + entry = AnnotationEntry.Create ("/comment"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect: {ex}"); + return; + } + Assert.That (entry, Is.EqualTo (AnnotationEntry.Comment), "/comment"); + + try { + entry = AnnotationEntry.Create ("/comment.priv"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect: {ex}"); + return; + } + Assert.That (entry, Is.EqualTo (AnnotationEntry.PrivateComment), "/comment.priv"); + + try { + entry = AnnotationEntry.Create ("/comment.shared"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect: {ex}"); + return; + } + Assert.That (entry, Is.EqualTo (AnnotationEntry.SharedComment), "/comment.shared"); + + try { + entry = AnnotationEntry.Create ("/flags"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect: {ex}"); + return; + } + Assert.That (entry, Is.EqualTo (AnnotationEntry.Flags), "/flags"); + + try { + entry = AnnotationEntry.Create ("/flags.priv"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect: {ex}"); + return; + } + Assert.That (entry, Is.EqualTo (AnnotationEntry.PrivateFlags), "/flags.priv"); + + try { + entry = AnnotationEntry.Create ("/flags.shared"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect: {ex}"); + return; + } + Assert.That (entry, Is.EqualTo (AnnotationEntry.SharedFlags), "/flags.shared"); + + try { + entry = AnnotationEntry.Create ("/altsubject"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect: {ex}"); + return; + } + Assert.That (entry, Is.EqualTo (AnnotationEntry.AltSubject), "/altsubject"); + + try { + entry = AnnotationEntry.Create ("/altsubject.priv"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect: {ex}"); + return; + } + Assert.That (entry, Is.EqualTo (AnnotationEntry.PrivateAltSubject), "/altsubject.priv"); + + try { + entry = AnnotationEntry.Create ("/altsubject.shared"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect: {ex}"); + return; + } + Assert.That (entry, Is.EqualTo (AnnotationEntry.SharedAltSubject), "/altsubject.shared"); + + try { + entry = AnnotationEntry.Create ("/1.2.3.4/comment"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect: {ex}"); + return; + } + Assert.That (entry.Entry, Is.EqualTo ("/1.2.3.4/comment"), "Entry"); + Assert.That (entry.PartSpecifier, Is.EqualTo ("1.2.3.4"), "PartSpecifier"); + Assert.That (entry.Path, Is.EqualTo ("/comment"), "Path"); + Assert.That (entry.Scope, Is.EqualTo (AnnotationScope.Both), "Scope"); + } + } +} diff --git a/UnitTests/AnnotationTests.cs b/UnitTests/AnnotationTests.cs new file mode 100644 index 0000000000..6e423b5e44 --- /dev/null +++ b/UnitTests/AnnotationTests.cs @@ -0,0 +1,49 @@ +// +// AnnotationTests.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using MailKit; + +namespace UnitTests { + [TestFixture] + public class AnnotationTests + { + [Test] + public void TestArgumentExceptions () + { + Assert.Throws (() => new Annotation (null)); + } + + [Test] + public void TestBasicFunctionality () + { + var annotation = new Annotation (AnnotationEntry.AltSubject); + annotation.Properties.Add (AnnotationAttribute.SharedValue, "Shared altsubject"); + annotation.Properties.Add (AnnotationAttribute.PrivateValue, "Private altsubject"); + Assert.That (annotation.Entry, Is.EqualTo (AnnotationEntry.AltSubject), "Entry"); + Assert.That (annotation.Properties, Has.Count.EqualTo (2), "Count"); + } + } +} diff --git a/UnitTests/AppendRequestTests.cs b/UnitTests/AppendRequestTests.cs new file mode 100644 index 0000000000..3e324d0bc3 --- /dev/null +++ b/UnitTests/AppendRequestTests.cs @@ -0,0 +1,116 @@ +// +// AppendRequestTests.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using MimeKit; +using MailKit; + +namespace UnitTests { + [TestFixture] + public class AppendRequestTests + { + [Test] + public void TestArgumentExceptions () + { + var keywords = new string[] { "$Forwarded" }; + var message = new MimeMessage (); + + Assert.Throws (() => new AppendRequest (null)); + Assert.Throws (() => new AppendRequest (null, MessageFlags.Seen)); + Assert.Throws (() => new AppendRequest (null, MessageFlags.Seen, DateTimeOffset.Now)); + Assert.Throws (() => new AppendRequest (null, MessageFlags.Seen, keywords)); + Assert.Throws (() => new AppendRequest (message, MessageFlags.Seen, null)); + Assert.Throws (() => new AppendRequest (null, MessageFlags.Seen, keywords, DateTimeOffset.Now)); + Assert.Throws (() => new AppendRequest (message, MessageFlags.Seen, null, DateTimeOffset.Now)); + } + + [Test] + public void TestConstructors () + { + //var annotation = new Annotation (AnnotationEntry.AltSubject); + //annotation.Properties[AnnotationAttribute.PrivateValue] = string.Format ("Alternate subject"); + //var annotations = new Annotation[] { annotation }; + var keywords = new string[] { "$Forwarded", "$Junk" }; + var keywordSet = new HashSet (keywords); + var flags = MessageFlags.Seen | MessageFlags.Draft; + var internalDate = DateTimeOffset.Now; + var message = new MimeMessage (); + AppendRequest request; + + request = new AppendRequest (message); + Assert.That (request.Message, Is.EqualTo (message), "Message #1"); + Assert.That (request.Flags, Is.EqualTo (MessageFlags.None), "Flags #1"); + Assert.That (request.Keywords, Is.Null, "Keywords #1"); + Assert.That (request.InternalDate, Is.Null, "InternalDate #1"); + Assert.That (request.Annotations, Is.Null, "Annotations #1"); + + request = new AppendRequest (message, flags); + Assert.That (request.Message, Is.EqualTo (message), "Message #2"); + Assert.That (request.Flags, Is.EqualTo (flags), "Flags #2"); + Assert.That (request.Keywords, Is.Null, "Keywords #2"); + Assert.That (request.InternalDate, Is.Null, "InternalDate #2"); + Assert.That (request.Annotations, Is.Null, "Annotations #2"); + + request = new AppendRequest (message, flags, keywords); + Assert.That (request.Message, Is.EqualTo (message), "Message #3"); + Assert.That (request.Flags, Is.EqualTo (flags), "Flags #3"); + Assert.That (request.Keywords, Is.InstanceOf> (), "Keywords Type #3"); + Assert.That (request.Keywords, Has.Count.EqualTo (keywords.Length), "Keywords #3"); + Assert.That (request.InternalDate, Is.Null, "InternalDate #3"); + Assert.That (request.Annotations, Is.Null, "Annotations #3"); + + request = new AppendRequest (message, flags, keywordSet); + Assert.That (request.Message, Is.EqualTo (message), "Message #4"); + Assert.That (request.Flags, Is.EqualTo (flags), "Flags #4"); + Assert.That (request.Keywords, Is.InstanceOf> (), "Keywords Type #4"); + Assert.That (request.Keywords, Is.EqualTo (keywordSet), "Keywords #4"); + Assert.That (request.InternalDate, Is.Null, "InternalDate #4"); + Assert.That (request.Annotations, Is.Null, "Annotations #4"); + + request = new AppendRequest (message, flags, internalDate); + Assert.That (request.Message, Is.EqualTo (message), "Message #5"); + Assert.That (request.Flags, Is.EqualTo (flags), "Flags #5"); + Assert.That (request.Keywords, Is.Null, "Keywords #5"); + Assert.That (request.InternalDate.Value, Is.EqualTo (internalDate), "InternalDate #5"); + Assert.That (request.Annotations, Is.Null, "Annotations #5"); + + request = new AppendRequest (message, flags, keywords, internalDate); + Assert.That (request.Message, Is.EqualTo (message), "Message #6"); + Assert.That (request.Flags, Is.EqualTo (flags), "Flags #6"); + Assert.That (request.Keywords, Is.InstanceOf> (), "Keywords Type #6"); + Assert.That (request.Keywords, Has.Count.EqualTo (keywords.Length), "Keywords #6"); + Assert.That (request.InternalDate.Value, Is.EqualTo (internalDate), "InternalDate #6"); + Assert.That (request.Annotations, Is.Null, "Annotations #6"); + + request = new AppendRequest (message, flags, keywordSet, internalDate); + Assert.That (request.Message, Is.EqualTo (message), "Message #7"); + Assert.That (request.Flags, Is.EqualTo (flags), "Flags #7"); + Assert.That (request.Keywords, Is.InstanceOf> (), "Keywords Type #7"); + Assert.That (request.Keywords, Is.EqualTo (keywordSet), "Keywords #7"); + Assert.That (request.InternalDate.Value, Is.EqualTo (internalDate), "InternalDate #7"); + Assert.That (request.Annotations, Is.Null, "Annotations #7"); + } + } +} diff --git a/UnitTests/ArgumentExceptionTests.cs b/UnitTests/ArgumentExceptionTests.cs index 38b554f652..0a9f30945e 100644 --- a/UnitTests/ArgumentExceptionTests.cs +++ b/UnitTests/ArgumentExceptionTests.cs @@ -3,7 +3,7 @@ // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -24,114 +24,23 @@ // THE SOFTWARE. // -using System; -using System.IO; -using System.Collections.Generic; - -using NUnit.Framework; - -using MimeKit; using MailKit; using MailKit.Search; -namespace UnitTests -{ +namespace UnitTests { [TestFixture] public class ArgumentExceptionTests { [Test] public void TestArgumentExceptions () { - var enumeratedRights = new [] { AccessRight.OpenFolder, AccessRight.CreateFolder }; - - Assert.Throws (() => new AccessControl (null)); - Assert.Throws (() => new AccessControl (null, "rk")); - Assert.Throws (() => new AccessControl (null, enumeratedRights)); - Assert.Throws (() => new AccessControl ("name", (string) null)); - Assert.Throws (() => new AccessControl ("name", (IEnumerable) null)); - - Assert.Throws (() => new AccessControlList (null)); - - Assert.Throws (() => new AccessRights ((IEnumerable) null)); - Assert.Throws (() => new AccessRights ((string) null)); - - var rights = new AccessRights (); - Assert.Throws (() => rights.AddRange ((string) null)); - Assert.Throws (() => rights.AddRange ((IEnumerable) null)); - - Assert.Throws (() => new AlertEventArgs (null)); - - Assert.Throws (() => new FolderNamespace ('.', null)); - - var namespaces = new FolderNamespaceCollection (); - FolderNamespace ns; - - Assert.Throws (() => namespaces.Add (null)); - Assert.Throws (() => namespaces.Contains (null)); - Assert.Throws (() => namespaces.Remove (null)); - Assert.Throws (() => ns = namespaces[-1]); - Assert.Throws (() => namespaces[-1] = new FolderNamespace ('.', "")); - - namespaces.Add (new FolderNamespace ('.', "")); - Assert.Throws (() => namespaces[0] = null); - - Assert.Throws (() => new FolderNotFoundException (null)); - Assert.Throws (() => new FolderNotFoundException ("message", null)); - Assert.Throws (() => new FolderNotFoundException ("message", null, new Exception ("message"))); - - Assert.Throws (() => new FolderNotOpenException (null, FolderAccess.ReadOnly)); - Assert.Throws (() => new FolderNotOpenException (null, FolderAccess.ReadOnly, "message")); - Assert.Throws (() => new FolderNotOpenException (null, FolderAccess.ReadOnly, "message", new Exception ("message"))); - - Assert.Throws (() => new FolderRenamedEventArgs (null, "name")); - Assert.Throws (() => new FolderRenamedEventArgs ("name", null)); - - Assert.Throws (() => new MessageEventArgs (-1)); - - Assert.Throws (() => new MessageFlagsChangedEventArgs (0, MessageFlags.Answered, null)); - Assert.Throws (() => new MessageFlagsChangedEventArgs (0, MessageFlags.Answered, null, 1)); - Assert.Throws (() => new MessageFlagsChangedEventArgs (0, UniqueId.MinValue, MessageFlags.Answered, null)); - Assert.Throws (() => new MessageFlagsChangedEventArgs (0, UniqueId.MinValue, MessageFlags.Answered, null, 1)); - - Assert.Throws (() => new MessageLabelsChangedEventArgs (0, null)); - Assert.Throws (() => new MessageLabelsChangedEventArgs (0, null, 1)); - Assert.Throws (() => new MessageLabelsChangedEventArgs (0, UniqueId.MinValue, null)); - Assert.Throws (() => new MessageLabelsChangedEventArgs (0, UniqueId.MinValue, null, 1)); - - Assert.Throws (() => new MessageSentEventArgs (null, "response")); - Assert.Throws (() => new MessageSentEventArgs (new MimeMessage (), null)); - - Assert.Throws (() => new MessageSummaryFetchedEventArgs (null)); - - Assert.Throws (() => new MessagesVanishedEventArgs (null, false)); - - Assert.Throws (() => new MetadataCollection (null)); - - var metadataOptions = new MetadataOptions (); - Assert.Throws (() => metadataOptions.Depth = 500); - - Assert.Throws (() => new ModSeqChangedEventArgs (-1)); - Assert.Throws (() => new ModSeqChangedEventArgs (-1, 1)); - Assert.Throws (() => new ModSeqChangedEventArgs (-1, UniqueId.MinValue, 1)); - Assert.Throws (() => new OrderBy (OrderByType.To, SortOrder.None)); - Assert.Throws (() => new ProtocolLogger ((string) null)); - Assert.Throws (() => new ProtocolLogger ((Stream) null)); - using (var logger = new ProtocolLogger (new MemoryStream ())) { - var buffer = new byte[1024]; - - Assert.Throws (() => logger.LogConnect (null)); - Assert.Throws (() => logger.LogClient (null, 0, 0)); - Assert.Throws (() => logger.LogServer (null, 0, 0)); - Assert.Throws (() => logger.LogClient (buffer, -1, 0)); - Assert.Throws (() => logger.LogServer (buffer, -1, 0)); - Assert.Throws (() => logger.LogClient (buffer, 0, -1)); - Assert.Throws (() => logger.LogServer (buffer, 0, -1)); - } - - Assert.Throws (() => new UniqueIdMap (null, new [] { UniqueId.MinValue })); - Assert.Throws (() => new UniqueIdMap (new [] { UniqueId.MinValue }, null)); + Assert.Throws (() => new OrderByAnnotation (null, AnnotationAttribute.PrivateValue, SortOrder.Ascending)); + Assert.Throws (() => new OrderByAnnotation (AnnotationEntry.AltSubject, null, SortOrder.Ascending)); + Assert.Throws (() => new OrderByAnnotation (AnnotationEntry.AltSubject, AnnotationAttribute.Size, SortOrder.Ascending)); + Assert.Throws (() => new OrderByAnnotation (AnnotationEntry.AltSubject, AnnotationAttribute.PrivateSize, SortOrder.Ascending)); + Assert.Throws (() => new OrderByAnnotation (AnnotationEntry.AltSubject, AnnotationAttribute.SharedSize, SortOrder.Ascending)); } } } diff --git a/UnitTests/BodyPartTests.cs b/UnitTests/BodyPartTests.cs index fff668ea09..7329238db1 100644 --- a/UnitTests/BodyPartTests.cs +++ b/UnitTests/BodyPartTests.cs @@ -1,9 +1,9 @@ -// +// // BodyPartTests.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -24,13 +24,10 @@ // THE SOFTWARE. // -using System.Collections.Generic; - -using NUnit.Framework; +using System.Text; +using System.Collections; using MimeKit; -using MimeKit.Utils; - using MailKit; // Note: These tests are for BodyPart and Envelope's custom format. While the format is similar @@ -42,58 +39,165 @@ namespace UnitTests { public class BodyPartTests { [Test] - public void TestSimplePlainTextBody () + public void TestBodyPartBasic () { - const string text = "(\"TEXT\" \"PLAIN\" (\"CHARSET\" \"US-ASCII\") NIL NIL \"7BIT\" 3028 NIL NIL NIL NIL 92)"; - BodyPartText basic; + var uri = new Uri ("https://www.nationalgeographic.com/travel/contests/photographer-of-the-year-2018/wallpapers/week-9-nature/2/"); + const string expected = "(\"image\" \"jpeg\" (\"name\" \"wallpaper.jpg\") \"id@localhost\" \"A majestic supercell storm approaching a house in Kansas, 2016.\" \"base64\" 0 \"8criUiOQmpfifOuOmYFtEQ==\" (\"attachment\" (\"filename\" \"wallpaper.jpg\")) (\"en\" \"fr\") \"https://www.nationalgeographic.com/travel/contests/photographer-of-the-year-2018/wallpapers/week-9-nature/2/\")"; + var contentType = new ContentType ("image", "jpeg") { + Name = "wallpaper.jpg" + }; + BodyPartBasic basic, parsed; BodyPart body; - Assert.IsTrue (BodyPart.TryParse (text, out body), "Failed to parse body."); - - Assert.IsInstanceOf (body, "Body types did not match."); - basic = (BodyPartText) body; - - Assert.IsTrue (body.ContentType.IsMimeType ("text", "plain"), "Content-Type did not match."); - Assert.AreEqual ("US-ASCII", body.ContentType.Parameters["charset"], "charset param did not match"); - - Assert.IsNotNull (basic, "The parsed body is not BodyPartText."); - Assert.AreEqual ("7BIT", basic.ContentTransferEncoding, "Content-Transfer-Encoding did not match."); - Assert.AreEqual (3028, basic.Octets, "Octet count did not match."); - Assert.AreEqual (92, basic.Lines, "Line count did not match."); + basic = new BodyPartBasic (contentType, string.Empty) { + ContentId = "id@localhost", + ContentMd5 = "8criUiOQmpfifOuOmYFtEQ==", + ContentLanguage = new string[] { "en", "fr" }, + ContentLocation = uri, + ContentDescription = "A majestic supercell storm approaching a house in Kansas, 2016.", + ContentDisposition = new ContentDisposition (ContentDisposition.Attachment) { + FileName = "wallpaper.jpg" + }, + ContentTransferEncoding = "base64" + }; + + Assert.That (basic.IsAttachment, Is.True); + Assert.That (basic.FileName, Is.EqualTo ("wallpaper.jpg")); + Assert.That (basic.ToString (), Is.EqualTo (expected)); + Assert.That (BodyPart.TryParse (expected, out body), Is.True); + Assert.That (body, Is.InstanceOf ()); + + parsed = (BodyPartBasic) body; + Assert.That (parsed.ToString (), Is.EqualTo (expected)); } [Test] - public void TestExampleEnvelopeRfc3501 () + public void TestNilSerialization () { - const string text = "(\"Wed, 17 Jul 1996 02:23:25 -0700 (PDT)\" \"IMAP4rev1 WG mtg summary and minutes\" ((\"Terry Gray\" NIL \"gray\" \"cac.washington.edu\")) ((\"Terry Gray\" NIL \"gray\" \"cac.washington.edu\")) ((\"Terry Gray\" NIL \"gray\" \"cac.washington.edu\")) ((NIL NIL \"imap\" \"cac.washington.edu\")) ((NIL NIL \"minutes\" \"CNRI.Reston.VA.US\") (\"John Klensin\" NIL \"KLENSIN\" \"MIT.EDU\")) NIL NIL \"\")"; - Envelope envelope; - - Assert.IsTrue (Envelope.TryParse (text, out envelope), "Failed to parse envelope."); + var builder = new StringBuilder (); - Assert.IsTrue (envelope.Date.HasValue, "Parsed ENVELOPE date is null."); - Assert.AreEqual ("Wed, 17 Jul 1996 02:23:25 -0700", DateUtils.FormatDate (envelope.Date.Value), "Date does not match."); - Assert.AreEqual ("IMAP4rev1 WG mtg summary and minutes", envelope.Subject, "Subject does not match."); + BodyPart.Encode (builder, (BodyPart) null); + Assert.That (builder.ToString (), Is.EqualTo ("NIL"), "BodyPart"); - Assert.AreEqual (1, envelope.From.Count, "From counts do not match."); - Assert.AreEqual ("\"Terry Gray\" ", envelope.From.ToString (), "From does not match."); + builder.Clear (); + BodyPart.Encode (builder, (BodyPartCollection) null); + Assert.That (builder.ToString (), Is.EqualTo ("NIL"), "BodyPartCollection"); - Assert.AreEqual (1, envelope.Sender.Count, "Sender counts do not match."); - Assert.AreEqual ("\"Terry Gray\" ", envelope.Sender.ToString (), "Sender does not match."); + builder.Clear (); + BodyPart.Encode (builder, (ContentDisposition) null); + Assert.That (builder.ToString (), Is.EqualTo ("NIL"), "ContentDisposition"); - Assert.AreEqual (1, envelope.ReplyTo.Count, "Reply-To counts do not match."); - Assert.AreEqual ("\"Terry Gray\" ", envelope.ReplyTo.ToString (), "Reply-To does not match."); + //builder.Clear (); + //BodyPart.Encode (builder, (ContentType) null); + //Assert.That (builder.ToString (), Is.EqualTo ("NIL"), "ContentType"); - Assert.AreEqual (1, envelope.To.Count, "To counts do not match."); - Assert.AreEqual ("imap@cac.washington.edu", envelope.To.ToString (), "To does not match."); + builder.Clear (); + BodyPart.Encode (builder, (Envelope) null); + Assert.That (builder.ToString (), Is.EqualTo ("NIL"), "Envelope"); - Assert.AreEqual (2, envelope.Cc.Count, "Cc counts do not match."); - Assert.AreEqual ("minutes@CNRI.Reston.VA.US, \"John Klensin\" ", envelope.Cc.ToString (), "Cc does not match."); + builder.Clear (); + BodyPart.Encode (builder, (IList) null); + Assert.That (builder.ToString (), Is.EqualTo ("NIL"), "IEnumerable"); + } - Assert.AreEqual (0, envelope.Bcc.Count, "Bcc counts do not match."); + [Test] + public void TestSimplePlainTextBody () + { + const string expected = "(\"text\" \"plain\" (\"charset\" \"us-ascii\" \"name\" \"body.txt\") NIL NIL \"7bit\" 3028 NIL NIL NIL NIL 92)"; + var contentType = new ContentType ("text", "plain") { Charset = "us-ascii", Name = "body.txt" }; + BodyPartText text, parsed; + BodyPart body; - Assert.IsNull (envelope.InReplyTo, "In-Reply-To is not null."); + text = new BodyPartText (contentType, string.Empty) { + ContentTransferEncoding = "7bit", + Octets = 3028, + Lines = 92, + }; + + Assert.That (text.IsPlain, Is.True); + Assert.That (text.IsHtml, Is.False); + Assert.That (text.IsAttachment, Is.False); + Assert.That (text.FileName, Is.EqualTo ("body.txt")); + Assert.That (text.ToString (), Is.EqualTo (expected)); + Assert.That (BodyPart.TryParse (expected, out body), Is.True); + Assert.That (body, Is.InstanceOf ()); + + parsed = (BodyPartText) body; + Assert.That (parsed.ContentType.IsMimeType ("text", "plain"), Is.True, "Content-Type did not match."); + Assert.That (parsed.ContentType.Charset, Is.EqualTo ("us-ascii"), "charset param did not match"); + Assert.That (parsed.ContentType.Name, Is.EqualTo ("body.txt"), "name param did not match"); + Assert.That (parsed.ContentTransferEncoding, Is.EqualTo ("7bit"), "Content-Transfer-Encoding did not match."); + Assert.That (parsed.Octets, Is.EqualTo (3028), "Octet count did not match."); + Assert.That (parsed.Lines, Is.EqualTo (92), "Line count did not match."); + Assert.That (parsed.ToString (), Is.EqualTo (expected)); + } - Assert.AreEqual ("B27397-0100000@cac.washington.edu", envelope.MessageId, "Message-Id does not match."); + [Test] + public void TestBodyPartCollection () + { + var text = new BodyPartText (new ContentType ("text", "plain"), string.Empty) { ContentLocation = new Uri ("body", UriKind.Relative) }; + var image1 = new BodyPartBasic (new ContentType ("image", "jpeg"), string.Empty) { ContentLocation = new Uri ("http://localhost/image1.jpg") }; + var image2 = new BodyPartBasic (new ContentType ("image", "jpeg"), string.Empty) { ContentId = "image2@localhost" }; + var list = new BodyPartCollection (); + var parts = new BodyPart[3]; + int i = 0; + + Assert.Throws (() => list.Add (null)); + Assert.Throws (() => list.Remove (null)); + Assert.Throws (() => list.Contains (null)); + Assert.Throws (() => list.IndexOf (null)); + Assert.Throws (() => list.CopyTo (null, 0)); + Assert.Throws (() => list.CopyTo (parts, -1)); + Assert.Throws (() => { var x = list[0]; }); + + Assert.That (list.IsReadOnly, Is.False); + Assert.That (list, Is.Empty); + + list.Add (text); + Assert.That (list, Has.Count.EqualTo (1)); + Assert.That (list, Does.Contain (text)); + Assert.That (list, Does.Not.Contain (image1)); + Assert.That (list.IndexOf (new Uri ("body", UriKind.Relative)), Is.EqualTo (0)); + Assert.That (list.IndexOf (new Uri ("http://localhost/image1.jpg")), Is.EqualTo (-1)); + Assert.That (list.IndexOf (new Uri ("cid:image2@localhost")), Is.EqualTo (-1)); + Assert.That (list[0], Is.EqualTo (text)); + + list.Add (image1); + Assert.That (list, Has.Count.EqualTo (2)); + Assert.That (list, Does.Contain (text)); + Assert.That (list, Does.Contain (image1)); + Assert.That (list.IndexOf (new Uri ("body", UriKind.Relative)), Is.EqualTo (0)); + Assert.That (list.IndexOf (new Uri ("http://localhost/image1.jpg")), Is.EqualTo (1)); + Assert.That (list.IndexOf (new Uri ("cid:image2@localhost")), Is.EqualTo (-1)); + Assert.That (list[0], Is.EqualTo (text)); + Assert.That (list[1], Is.EqualTo (image1)); + + Assert.That (list.Remove (text), Is.True); + Assert.That (list, Has.Count.EqualTo (1)); + Assert.That (list, Does.Not.Contain (text)); + Assert.That (list, Does.Contain (image1)); + Assert.That (list.IndexOf (new Uri ("body", UriKind.Relative)), Is.EqualTo (-1)); + Assert.That (list.IndexOf (new Uri ("http://localhost/image1.jpg")), Is.EqualTo (0)); + Assert.That (list.IndexOf (new Uri ("cid:image2@localhost")), Is.EqualTo (-1)); + Assert.That (list[0], Is.EqualTo (image1)); + + list.Clear (); + Assert.That (list, Is.Empty); + + list.Add (text); + list.Add (image1); + list.Add (image2); + list.CopyTo (parts, 0); + Assert.That (list.IndexOf (new Uri ("body", UriKind.Relative)), Is.EqualTo (0)); + Assert.That (list.IndexOf (new Uri ("http://localhost/image1.jpg")), Is.EqualTo (1)); + Assert.That (list.IndexOf (new Uri ("cid:image2@localhost")), Is.EqualTo (2)); + + foreach (var part in list) + Assert.That (part, Is.EqualTo (parts[i++])); + + i = 0; + foreach (var part in (IEnumerable) list) + Assert.That (part, Is.EqualTo (parts[i++])); } [Test] @@ -103,21 +207,44 @@ public void TestNestedBodyStructure () BodyPartMultipart multipart; BodyPart body; - Assert.IsTrue (BodyPart.TryParse (text, out body), "Failed to parse body."); + Assert.That (BodyPart.TryParse (text, out body), Is.True, "Failed to parse body."); - Assert.IsInstanceOf (body, "Body types did not match."); + Assert.That (body, Is.InstanceOf (), "Body types did not match."); multipart = (BodyPartMultipart) body; - Assert.IsTrue (body.ContentType.IsMimeType ("multipart", "mixed"), "Content-Type did not match."); - Assert.AreEqual ("----=_NextPart_000_0077_01CBB179.57530990", body.ContentType.Parameters["boundary"], "boundary param did not match"); - Assert.AreEqual (3, multipart.BodyParts.Count, "BodyParts count does not match."); - Assert.IsInstanceOf (multipart.BodyParts[0], "The type of the first child does not match."); - Assert.IsInstanceOf (multipart.BodyParts[1], "The type of the second child does not match."); - Assert.IsInstanceOf (multipart.BodyParts[2], "The type of the third child does not match."); + Assert.That (body.ContentType.IsMimeType ("multipart", "mixed"), Is.True, "Content-Type did not match."); + Assert.That (body.ContentType.Parameters["boundary"], Is.EqualTo ("----=_NextPart_000_0077_01CBB179.57530990"), "boundary param did not match"); + Assert.That (multipart.BodyParts, Has.Count.EqualTo (3), "BodyParts count does not match."); + Assert.That (multipart.BodyParts[0], Is.InstanceOf (), "The type of the first child does not match."); + Assert.That (multipart.BodyParts[1], Is.InstanceOf (), "The type of the second child does not match."); + Assert.That (multipart.BodyParts[2], Is.InstanceOf (), "The type of the third child does not match."); // FIXME: assert more stuff } + [Test] + public void TestMultipartWithNoChildren () + { + var original = new BodyPartMultipart (new ContentType ("multipart", "mixed") { Boundary = "----=_NextPart_000_001" }, string.Empty); + original.BodyParts.Add (new BodyPartMultipart (new ContentType ("multipart", "alternative") { Boundary = "----=_AlternativePart_001_001" }, string.Empty)); + + var serialized = original.ToString (); + + Assert.That (BodyPart.TryParse (serialized, out var body), Is.True, "Failed to parse."); + Assert.That (body, Is.InstanceOf (), "Body types did not match."); + + var multipart = (BodyPartMultipart) body; + Assert.That (multipart.ContentType.IsMimeType ("multipart", "mixed"), Is.True, "Content-Type did not match."); + Assert.That (multipart.ContentType.Boundary, Is.EqualTo (original.ContentType.Boundary), "boundary param did not match"); + Assert.That (multipart.BodyParts, Has.Count.EqualTo (1), "BodyParts count does not match."); + Assert.That (multipart.BodyParts[0], Is.InstanceOf (), "The type of the first child does not match."); + + var alternative = (BodyPartMultipart) multipart.BodyParts[0]; + Assert.That (alternative.ContentType.IsMimeType ("multipart", "alternative"), Is.True, "Inner Content-Type did not match."); + Assert.That (alternative.ContentType.Boundary, Is.EqualTo (original.BodyParts[0].ContentType.Boundary), "Inner boundary param did not match"); + Assert.That (alternative.BodyParts, Is.Empty, "Inner BodyParts count does not match."); + } + static ContentType CreateContentType (string type, string subtype, string partSpecifier) { var contentType = new ContentType (type, subtype); @@ -127,14 +254,15 @@ static ContentType CreateContentType (string type, string subtype, string partSp static BodyPartMessage CreateMessage (string type, string subtype, string partSpecifier, BodyPart body) { - var message = new BodyPartMessage { ContentType = CreateContentType (type, subtype, partSpecifier) }; - message.Body = body; + var message = new BodyPartMessage (CreateContentType (type, subtype, partSpecifier), partSpecifier) { + Body = body + }; return message; } static BodyPartMultipart CreateMultipart (string type, string subtype, string partSpecifier, params BodyPart[] bodyParts) { - var multipart = new BodyPartMultipart { ContentType = CreateContentType (type, subtype, partSpecifier) }; + var multipart = new BodyPartMultipart (CreateContentType (type, subtype, partSpecifier), partSpecifier); foreach (var bodyPart in bodyParts) multipart.BodyParts.Add (bodyPart); return multipart; @@ -142,37 +270,80 @@ static BodyPartMultipart CreateMultipart (string type, string subtype, string pa static BodyPartBasic CreateBasic (string type, string subtype, string partSpecifier) { - return new BodyPartBasic { ContentType = CreateContentType (type, subtype, partSpecifier) }; + return new BodyPartBasic (CreateContentType (type, subtype, partSpecifier), partSpecifier); } - static BodyPartBasic CreateText (string type, string subtype, string partSpecifier) + static BodyPartText CreateText (string type, string subtype, string partSpecifier) { - return new BodyPartText { ContentType = CreateContentType (type, subtype, partSpecifier) }; + return new BodyPartText (CreateContentType (type, subtype, partSpecifier), partSpecifier); } static void VerifyPartSpecifier (BodyPart part) { var expected = part.ContentType.Parameters["part-specifier"]; - Assert.AreEqual (expected, part.PartSpecifier, "The part-specifier does not match for {0}", part.ContentType.MimeType); + Assert.That (part.PartSpecifier, Is.EqualTo (expected), $"The part-specifier does not match for {part.ContentType.MimeType}"); - var message = part as BodyPartMessage; - if (message != null) { + if (part is BodyPartMessage message) { VerifyPartSpecifier (message.Body); return; } - var multipart = part as BodyPartMultipart; - if (multipart != null) { + if (part is BodyPartMultipart multipart) { for (int i = 0; i < multipart.BodyParts.Count; i++) VerifyPartSpecifier (multipart.BodyParts[i]); return; } } + class TestVisitor : BodyPartVisitor + { + readonly StringBuilder builder = new StringBuilder (); + int indent; + + public override void Visit (BodyPart body) + { + builder.Length = 0; + indent = 0; + + base.Visit (body); + } + + protected internal override void VisitBodyPart (BodyPart entity) + { + builder.Append (' ', indent); + builder.Append (entity.ContentType.MimeType); + builder.Append ('\n'); + + base.VisitBodyPart (entity); + } + + protected override void VisitMessage (BodyPart message) + { + indent++; + base.VisitMessage (message); + indent--; + } + + protected override void VisitChildren (BodyPartMultipart multipart) + { + indent++; + base.VisitChildren (multipart); + indent--; + } + + public override string ToString () + { + return builder.ToString (); + } + } + [Test] public void TestComplexPartSpecifiersExampleRfc3501 () { + const string expected = "MULTIPART/MIXED\n TEXT/PLAIN\n APPLICATION/OCTET-STREAM\n MESSAGE/RFC822\n MULTIPART/MIXED\n TEXT/PLAIN\n APPLICATION/OCTET-STREAM\n MULTIPART/MIXED\n IMAGE/GIF\n MESSAGE/RFC822\n MULTIPART/MIXED\n TEXT/PLAIN\n MULTIPART/ALTERNATIVE\n TEXT/PLAIN\n TEXT/RICHTEXT\n"; + var visitor = new TestVisitor (); + BodyPart body = CreateMultipart ("MULTIPART", "MIXED", "", CreateText ("TEXT", "PLAIN", "1"), CreateBasic ("APPLICATION", "OCTET-STREAM", "2"), @@ -196,9 +367,18 @@ public void TestComplexPartSpecifiersExampleRfc3501 () ) ); + visitor.Visit (body); + + Assert.That (visitor.ToString (), Is.EqualTo (expected)); + Assert.Throws (() => new BodyPartText (new ContentType ("text", "plain"), string.Empty).Accept (null)); + Assert.Throws (() => new BodyPartBasic (new ContentType ("image", "jpeg"), string.Empty).Accept (null)); + Assert.Throws (() => new BodyPartMessage (new ContentType ("message", "rfc822"), string.Empty).Accept (null)); + Assert.Throws (() => new BodyPartMultipart (new ContentType ("multipart", "mixed"), string.Empty).Accept (null)); + var encoded = body.ToString (); - Assert.IsTrue (BodyPart.TryParse (encoded, out body)); + Assert.Throws (() => BodyPart.TryParse (null, out body)); + Assert.That (BodyPart.TryParse (encoded, out body), Is.True); VerifyPartSpecifier (body); } diff --git a/UnitTests/CompressedStreamTests.cs b/UnitTests/CompressedStreamTests.cs new file mode 100644 index 0000000000..29a7cf58d7 --- /dev/null +++ b/UnitTests/CompressedStreamTests.cs @@ -0,0 +1,155 @@ +// +// CompressedStreamTests.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System.Text; + +using MailKit; + +using UnitTests.Net; + +namespace UnitTests { + [TestFixture] + public class CompressedStreamTests + { + [Test] + public void TestArgumentExceptions () + { + using (var stream = new CompressedStream (new DummyNetworkStream ())) { + var buffer = new byte[16]; + + Assert.Throws (() => stream.Read (null, 0, buffer.Length)); + Assert.Throws (() => stream.Read (buffer, -1, buffer.Length)); + Assert.Throws (() => stream.Read (buffer, 0, -1)); + Assert.That (stream.Read (buffer, 0, 0), Is.EqualTo (0)); + + Assert.ThrowsAsync (async () => await stream.ReadAsync (null, 0, buffer.Length)); + Assert.ThrowsAsync (async () => await stream.ReadAsync (buffer, -1, buffer.Length)); + Assert.ThrowsAsync (async () => await stream.ReadAsync (buffer, 0, -1)); + + Assert.Throws (() => stream.Write (null, 0, buffer.Length)); + Assert.Throws (() => stream.Write (buffer, -1, buffer.Length)); + Assert.Throws (() => stream.Write (buffer, 0, -1)); + stream.Write (buffer, 0, 0); + + Assert.ThrowsAsync (async () => await stream.WriteAsync (null, 0, buffer.Length)); + Assert.ThrowsAsync (async () => await stream.WriteAsync (buffer, -1, buffer.Length)); + Assert.ThrowsAsync (async () => await stream.WriteAsync (buffer, 0, -1)); + } + } + + [Test] + public void TestCanReadWriteSeek () + { + using (var stream = new CompressedStream (new DummyNetworkStream ())) { + Assert.That (stream.CanRead, Is.True); + Assert.That (stream.CanWrite, Is.True); + Assert.That (stream.CanSeek, Is.False); + Assert.That (stream.CanTimeout, Is.True); + } + } + + [Test] + public void TestGetSetTimeouts () + { + using (var stream = new CompressedStream (new DummyNetworkStream ())) { + stream.ReadTimeout = 5; + Assert.That (stream.ReadTimeout, Is.EqualTo (5), "ReadTimeout"); + + stream.WriteTimeout = 7; + Assert.That (stream.WriteTimeout, Is.EqualTo (7), "WriteTimeout"); + } + } + + [Test] + public void TestReadWrite () + { + using (var stream = new CompressedStream (new DummyNetworkStream ())) { + string command = "A00000001 APPEND INBOX (\\Seen \\Draft) {4096+}\r\nFrom: Sample Sender \r\nTo: Sample Recipient \r\nSubject: This is a test message...\r\nDate: Mon, 22 Oct 2018 18:22:56 EDT\r\nMessage-Id: \r\n\r\nTesting... 1. 2. 3.\r\nTesting.\r\nOver and out.\r\n"; + var output = Encoding.ASCII.GetBytes (command); + const int compressedLength = 221; + var buffer = new byte[1024]; + int n; + + stream.Write (output, 0, output.Length); + stream.Flush (); + + Assert.That (stream.InnerStream.Position, Is.EqualTo (compressedLength), "Compressed output length"); + + stream.InnerStream.Position = 0; + + n = stream.Read (buffer, 0, buffer.Length); + Assert.That (n, Is.EqualTo (output.Length), "Decompressed input length"); + + var text = Encoding.ASCII.GetString (buffer, 0, n); + Assert.That (text, Is.EqualTo (command)); + } + } + + [Test] + public async Task TestReadWriteAsync () + { + using (var stream = new CompressedStream (new DummyNetworkStream ())) { + string command = "A00000001 APPEND INBOX (\\Seen \\Draft) {4096+}\r\nFrom: Sample Sender \r\nTo: Sample Recipient \r\nSubject: This is a test message...\r\nDate: Mon, 22 Oct 2018 18:22:56 EDT\r\nMessage-Id: \r\n\r\nTesting... 1. 2. 3.\r\nTesting.\r\nOver and out.\r\n"; + var output = Encoding.ASCII.GetBytes (command); + const int compressedLength = 221; + var buffer = new byte[1024]; + int n; + + await stream.WriteAsync (output, 0, output.Length); + await stream.FlushAsync (); + + Assert.That (stream.InnerStream.Position, Is.EqualTo (compressedLength), "Compressed output length"); + + stream.InnerStream.Position = 0; + + n = await stream.ReadAsync (buffer, 0, buffer.Length); + Assert.That (n, Is.EqualTo (output.Length), "Decompressed input length"); + + var text = Encoding.ASCII.GetString (buffer, 0, n); + Assert.That (text, Is.EqualTo (command)); + } + } + + [Test] + public void TestSeek () + { + using (var stream = new CompressedStream (new DummyNetworkStream ())) { + Assert.Throws (() => stream.Seek (0, SeekOrigin.Begin)); + Assert.Throws (() => { var x = stream.Position; }); + Assert.Throws (() => stream.Position = 500); + } + } + + [Test] + public void TestSetLength () + { + using (var stream = new CompressedStream (new DummyNetworkStream ())) { + Assert.Throws (() => { var x = stream.Length; }); + Assert.Throws (() => stream.SetLength (500)); + } + } + } +} diff --git a/UnitTests/DuplexStreamTests.cs b/UnitTests/DuplexStreamTests.cs new file mode 100644 index 0000000000..8c7723e959 --- /dev/null +++ b/UnitTests/DuplexStreamTests.cs @@ -0,0 +1,158 @@ +// +// DuplexStreamTests.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using MailKit; + +using UnitTests.Net; + +namespace UnitTests { + [TestFixture] + public class DuplexStreamTests + { + [Test] + public void TestArgumentExceptions () + { + Assert.Throws (() => new DuplexStream (null, Stream.Null)); + Assert.Throws (() => new DuplexStream (Stream.Null, null)); + + using (var stream = new DuplexStream (new DummyNetworkStream (), new DummyNetworkStream ())) { + var buffer = new byte[16]; + + Assert.Throws (() => stream.Read (null, 0, buffer.Length)); + Assert.Throws (() => stream.Read (buffer, -1, buffer.Length)); + Assert.Throws (() => stream.Read (buffer, 0, -1)); + + Assert.ThrowsAsync (async () => await stream.ReadAsync (null, 0, buffer.Length)); + Assert.ThrowsAsync (async () => await stream.ReadAsync (buffer, -1, buffer.Length)); + Assert.ThrowsAsync (async () => await stream.ReadAsync (buffer, 0, -1)); + + Assert.Throws (() => stream.Write (null, 0, buffer.Length)); + Assert.Throws (() => stream.Write (buffer, -1, buffer.Length)); + Assert.Throws (() => stream.Write (buffer, 0, -1)); + + Assert.ThrowsAsync (async () => await stream.WriteAsync (null, 0, buffer.Length)); + Assert.ThrowsAsync (async () => await stream.WriteAsync (buffer, -1, buffer.Length)); + Assert.ThrowsAsync (async () => await stream.WriteAsync (buffer, 0, -1)); + } + } + + [Test] + public void TestCanReadWriteSeek () + { + using (var stream = new DuplexStream (new DummyNetworkStream (), new DummyNetworkStream ())) { + Assert.That (stream.CanRead, Is.True); + Assert.That (stream.CanWrite, Is.True); + Assert.That (stream.CanSeek, Is.False); + Assert.That (stream.CanTimeout, Is.True); + } + } + + [Test] + public void TestGetSetTimeouts () + { + using (var stream = new DuplexStream (new DummyNetworkStream (), new DummyNetworkStream ())) { + stream.ReadTimeout = 5; + Assert.That (stream.ReadTimeout, Is.EqualTo (5), "ReadTimeout"); + + stream.WriteTimeout = 7; + Assert.That (stream.WriteTimeout, Is.EqualTo (7), "WriteTimeout"); + } + } + + [Test] + public void TestRead () + { + using (var stream = new DuplexStream (new DummyNetworkStream (), new DummyNetworkStream ())) { + var buffer = new byte[1024]; + int n; + + stream.InputStream.Write (buffer, 0, buffer.Length); + stream.InputStream.Position = 0; + + n = stream.Read (buffer, 0, buffer.Length); + Assert.That (n, Is.EqualTo (buffer.Length)); + } + } + + [Test] + public async Task TestReadAsync () + { + using (var stream = new DuplexStream (new DummyNetworkStream (), new DummyNetworkStream ())) { + var buffer = new byte[1024]; + int n; + + stream.InputStream.Write (buffer, 0, buffer.Length); + stream.InputStream.Position = 0; + + n = await stream.ReadAsync (buffer, 0, buffer.Length); + Assert.That (n, Is.EqualTo (buffer.Length)); + } + } + + [Test] + public void TestSeek () + { + using (var stream = new DuplexStream (new DummyNetworkStream (), new DummyNetworkStream ())) { + Assert.Throws (() => stream.Seek (0, SeekOrigin.Begin)); + Assert.Throws (() => { var x = stream.Position; }); + Assert.Throws (() => stream.Position = 500); + } + } + + [Test] + public void TestSetLength () + { + using (var stream = new DuplexStream (new DummyNetworkStream (), new DummyNetworkStream ())) { + Assert.Throws (() => { var x = stream.Length; }); + Assert.Throws (() => stream.SetLength (500)); + } + } + + [Test] + public void TestWrite () + { + using (var stream = new DuplexStream (new DummyNetworkStream (), new DummyNetworkStream ())) { + var buffer = new byte[1024]; + + stream.Write (buffer, 0, buffer.Length); + stream.Flush (); + Assert.That (stream.OutputStream.Position, Is.EqualTo (buffer.Length)); + } + } + + [Test] + public async Task TestWriteAsync () + { + using (var stream = new DuplexStream (new DummyNetworkStream (), new DummyNetworkStream ())) { + var buffer = new byte[1024]; + + await stream.WriteAsync (buffer, 0, buffer.Length); + await stream.FlushAsync (); + Assert.That (stream.OutputStream.Position, Is.EqualTo (buffer.Length)); + } + } + } +} diff --git a/UnitTests/EnvelopeTests.cs b/UnitTests/EnvelopeTests.cs index 9a0c461113..b8ee9d2bf4 100644 --- a/UnitTests/EnvelopeTests.cs +++ b/UnitTests/EnvelopeTests.cs @@ -3,7 +3,7 @@ // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -24,24 +24,19 @@ // THE SOFTWARE. // -using System; - -using NUnit.Framework; - using MimeKit; +using MimeKit.Utils; + using MailKit; -namespace UnitTests -{ +namespace UnitTests { [TestFixture] public class EnvelopeTests { [Test] public void TestArgumentExceptions () { - Envelope envelope; - - Assert.Throws (() => Envelope.TryParse (null, out envelope)); + Assert.Throws (() => Envelope.TryParse (null, out _)); } [Test] @@ -58,16 +53,116 @@ public void TestSerialization () original.Sender.Add (new MailboxAddress ("The Real Sender", "unit-tests@mimekit.org")); original.Subject = "This is the subject"; original.To.Add (new GroupAddress ("Group Address", new MailboxAddress[] { - new MailboxAddress ("Recipient 1", "unit-tests@mimekit.org"), - new MailboxAddress ("Recipient 2", "unit-tests@mimekit.org") + new MailboxAddress ("John \"Q.\" Recipient", "unit-tests@mimekit.org"), + new MailboxAddress ("Sarah Connor", "unit-tests@mimekit.org") })); var text = original.ToString (); Envelope envelope; - Assert.IsTrue (Envelope.TryParse (text, out envelope)); + Assert.That (Envelope.TryParse (text, out envelope), Is.True); + var text2 = envelope.ToString (); + + Assert.That (text2, Is.EqualTo (text)); + } + + [Test] + public void TestUnixAddressSerialization () + { + var original = new Envelope (); + original.Date = DateTimeOffset.Now; + original.From.Add (new MailboxAddress ((string) null, "fejj")); + original.To.Add (new MailboxAddress ((string) null, "notzed")); + original.InReplyTo = ""; + original.MessageId = ""; + original.ReplyTo.Add (new MailboxAddress ("Reply-To", "unit-tests@mimekit.org")); + original.Sender.Add (new MailboxAddress ("The Real Sender", string.Empty)); + original.Subject = "This is the subject"; + var text = original.ToString (); + + Assert.That (Envelope.TryParse (text, out var envelope), Is.True); + Assert.That (envelope.Sender.Mailboxes.First ().LocalPart, Is.EqualTo (string.Empty)); + Assert.That (envelope.From.Mailboxes.First ().LocalPart, Is.EqualTo ("fejj")); + Assert.That (envelope.To.Mailboxes.First ().LocalPart, Is.EqualTo ("notzed")); var text2 = envelope.ToString (); - Assert.AreEqual (text, text2); + Assert.That (text2, Is.EqualTo (text)); + } + + [Test] + public void TestExampleEnvelopeRfc3501 () + { + const string text = "(\"Wed, 17 Jul 1996 02:23:25 -0700 (PDT)\" \"IMAP4rev1 WG mtg summary and minutes\" ((\"Terry Gray\" NIL \"gray\" \"cac.washington.edu\")) ((\"Terry Gray\" NIL \"gray\" \"cac.washington.edu\")) ((\"Terry Gray\" NIL \"gray\" \"cac.washington.edu\")) ((NIL NIL \"imap\" \"cac.washington.edu\")) ((NIL NIL \"minutes\" \"CNRI.Reston.VA.US\") (\"John Klensin\" NIL \"KLENSIN\" \"MIT.EDU\")) NIL NIL \"\")"; + Envelope envelope; + + Assert.That (Envelope.TryParse (text, out envelope), Is.True, "Failed to parse envelope."); + + Assert.That (envelope.Date.HasValue, Is.True, "Parsed ENVELOPE date is null."); + Assert.That (DateUtils.FormatDate (envelope.Date.Value), Is.EqualTo ("Wed, 17 Jul 1996 02:23:25 -0700"), "Date does not match."); + Assert.That (envelope.Subject, Is.EqualTo ("IMAP4rev1 WG mtg summary and minutes"), "Subject does not match."); + + Assert.That (envelope.From, Has.Count.EqualTo (1), "From counts do not match."); + Assert.That (envelope.From.ToString (), Is.EqualTo ("\"Terry Gray\" "), "From does not match."); + + Assert.That (envelope.Sender, Has.Count.EqualTo (1), "Sender counts do not match."); + Assert.That (envelope.Sender.ToString (), Is.EqualTo ("\"Terry Gray\" "), "Sender does not match."); + + Assert.That (envelope.ReplyTo, Has.Count.EqualTo (1), "Reply-To counts do not match."); + Assert.That (envelope.ReplyTo.ToString (), Is.EqualTo ("\"Terry Gray\" "), "Reply-To does not match."); + + Assert.That (envelope.To, Has.Count.EqualTo (1), "To counts do not match."); + Assert.That (envelope.To.ToString (), Is.EqualTo ("imap@cac.washington.edu"), "To does not match."); + + Assert.That (envelope.Cc, Has.Count.EqualTo (2), "Cc counts do not match."); + Assert.That (envelope.Cc.ToString (), Is.EqualTo ("minutes@CNRI.Reston.VA.US, \"John Klensin\" "), "Cc does not match."); + + Assert.That (envelope.Bcc, Is.Empty, "Bcc counts do not match."); + + Assert.That (envelope.InReplyTo, Is.Null, "In-Reply-To is not null."); + + Assert.That (envelope.MessageId, Is.EqualTo ("B27397-0100000@cac.washington.edu"), "Message-Id does not match."); + } + + [Test] + public void TestEmptyEnvelope () + { + const string expected = "(NIL NIL NIL NIL NIL NIL NIL NIL NIL NIL)"; + var envelope = new Envelope (); + + Assert.That (envelope.ToString (), Is.EqualTo (expected)); + Assert.That (Envelope.TryParse (expected, out envelope), Is.True); + Assert.That (envelope.ToString (), Is.EqualTo (expected)); + } + + [Test] + public void TestGroupAddress () + { + const string expected = "(NIL NIL NIL NIL NIL ((NIL NIL \"Agents of Shield\" NIL)(\"Skye\" NIL \"skye\" \"shield.gov\")(\"Leo Fitz\" NIL \"fitz\" \"shield.gov\")(\"Melinda May\" NIL \"may\" \"shield.gov\")(NIL NIL NIL NIL)) NIL NIL NIL NIL)"; + var group = GroupAddress.Parse ("Agents of Shield: Skye , Leo Fitz , Melinda May ;"); + var envelope = new Envelope (); + + envelope.To.Add (group); + + Assert.That (envelope.ToString (), Is.EqualTo (expected)); + Assert.That (Envelope.TryParse (expected, out envelope), Is.True); + Assert.That (envelope.ToString (), Is.EqualTo (expected)); + Assert.That (envelope.To, Has.Count.EqualTo (1)); + Assert.That (envelope.To[0].ToString (), Is.EqualTo (group.ToString ())); + } + + [Test] + public void TestNestedGroupAddresses () + { + const string expected = "(NIL NIL NIL NIL NIL ((NIL NIL \"Agents of Shield\" NIL)(NIL NIL \"Mutants\" NIL)(\"Skye\" NIL \"skye\" \"shield.gov\")(NIL NIL NIL NIL)(\"Leo Fitz\" NIL \"fitz\" \"shield.gov\")(\"Melinda May\" NIL \"may\" \"shield.gov\")(NIL NIL NIL NIL)) NIL NIL NIL NIL)"; + var group = GroupAddress.Parse ("Agents of Shield: Mutants: Skye ;, Leo Fitz , Melinda May ;"); + var envelope = new Envelope (); + + envelope.To.Add (group); + + Assert.That (envelope.ToString (), Is.EqualTo (expected)); + Assert.That (Envelope.TryParse (expected, out envelope), Is.True); + Assert.That (envelope.ToString (), Is.EqualTo (expected)); + Assert.That (envelope.To, Has.Count.EqualTo (1)); + Assert.That (envelope.To[0].ToString (), Is.EqualTo (group.ToString ())); } } } diff --git a/UnitTests/EventArgsTests.cs b/UnitTests/EventArgsTests.cs new file mode 100644 index 0000000000..0d7f12135c --- /dev/null +++ b/UnitTests/EventArgsTests.cs @@ -0,0 +1,310 @@ +// +// EventArgsTests.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using MimeKit; +using MailKit; + +namespace UnitTests { + [TestFixture] + public class EventArgsTests + { + [Test] + public void TestAlertEventArgs () + { + var args = new AlertEventArgs ("Klingons on the starboard bow!"); + + Assert.That (args.Message, Is.EqualTo ("Klingons on the starboard bow!")); + + Assert.Throws (() => new AlertEventArgs (null)); + } + + [Test] + public void TestWebAlertEventArgs () + { + var args = new WebAlertEventArgs (new Uri ("http://www.google.com/"), "Klingons on the starboard bow!"); + + Assert.That (args.WebUri.AbsoluteUri, Is.EqualTo ("http://www.google.com/")); + Assert.That (args.Message, Is.EqualTo ("Klingons on the starboard bow!")); + + Assert.Throws (() => new WebAlertEventArgs (null, "message text.")); + Assert.Throws (() => new WebAlertEventArgs (new Uri ("http://www.google.com/"), null)); + } + + [Test] + public void TestAnnotationsChangedEventArgs () + { + var annotations = new List (); + var args = new AnnotationsChangedEventArgs (0, annotations); + Assert.That (args.Annotations, Is.Empty); + Assert.That (args.UniqueId.HasValue, Is.False); + Assert.That (args.ModSeq.HasValue, Is.False); + Assert.That (args.Index, Is.EqualTo (0)); + + Assert.Throws (() => new AnnotationsChangedEventArgs (0, null)); + } + + [Test] + public void TestAuthenticatedEventArgs () + { + var args = new AuthenticatedEventArgs ("Access Granted."); + + Assert.That (args.Message, Is.EqualTo ("Access Granted.")); + + Assert.Throws (() => new AuthenticatedEventArgs (null)); + } + + [Test] + public void TestFolderCreatedEventArgs () + { + Assert.Throws (() => new FolderCreatedEventArgs (null)); + } + + [Test] + public void TestFolderRenamedEventArgs () + { + var args = new FolderRenamedEventArgs ("Istanbul", "Constantinople"); + + Assert.That (args.OldName, Is.EqualTo ("Istanbul")); + Assert.That (args.NewName, Is.EqualTo ("Constantinople")); + + Assert.Throws (() => new FolderRenamedEventArgs (null, "name")); + Assert.Throws (() => new FolderRenamedEventArgs ("name", null)); + } + + [Test] + public void TestMessageEventArgs () + { + var args = new MessageEventArgs (0); + + Assert.That (args.Index, Is.EqualTo (0)); + + Assert.Throws (() => new MessageEventArgs (-1)); + } + + [Test] + public void TestMessageFlagsChangedEventArgs () + { + var keywords = new HashSet (new [] { "custom1", "custom2" }); + MessageFlagsChangedEventArgs args; + var uid = new UniqueId (5); + ulong modseq = 724; + + args = new MessageFlagsChangedEventArgs (0); + Assert.That (args.Keywords, Is.Empty); + Assert.That (args.Flags, Is.EqualTo (MessageFlags.None)); + Assert.That (args.UniqueId.HasValue, Is.False); + Assert.That (args.ModSeq.HasValue, Is.False); + Assert.That (args.Index, Is.EqualTo (0)); + + args = new MessageFlagsChangedEventArgs (0, MessageFlags.Answered); + Assert.That (args.Keywords, Is.Empty); + Assert.That (args.Flags, Is.EqualTo (MessageFlags.Answered)); + Assert.That (args.UniqueId.HasValue, Is.False); + Assert.That (args.ModSeq.HasValue, Is.False); + Assert.That (args.Index, Is.EqualTo (0)); + + args = new MessageFlagsChangedEventArgs (0, MessageFlags.Answered, modseq); + Assert.That (args.Keywords, Is.Empty); + Assert.That (args.Flags, Is.EqualTo (MessageFlags.Answered)); + Assert.That (args.UniqueId.HasValue, Is.False); + Assert.That (args.ModSeq, Is.EqualTo (modseq)); + Assert.That (args.Index, Is.EqualTo (0)); + + args = new MessageFlagsChangedEventArgs (0, MessageFlags.Answered, keywords); + Assert.That (args.Keywords, Has.Count.EqualTo (keywords.Count)); + Assert.That (args.Flags, Is.EqualTo (MessageFlags.Answered)); + Assert.That (args.UniqueId.HasValue, Is.False); + Assert.That (args.ModSeq.HasValue, Is.False); + Assert.That (args.Index, Is.EqualTo (0)); + + args = new MessageFlagsChangedEventArgs (0, MessageFlags.Answered, keywords, modseq); + Assert.That (args.Keywords, Has.Count.EqualTo (keywords.Count)); + Assert.That (args.Flags, Is.EqualTo (MessageFlags.Answered)); + Assert.That (args.UniqueId.HasValue, Is.False); + Assert.That (args.ModSeq, Is.EqualTo (modseq)); + Assert.That (args.Index, Is.EqualTo (0)); + + args = new MessageFlagsChangedEventArgs (0, uid, MessageFlags.Answered); + Assert.That (args.Keywords, Is.Empty); + Assert.That (args.Flags, Is.EqualTo (MessageFlags.Answered)); + Assert.That (args.UniqueId, Is.EqualTo (uid)); + Assert.That (args.ModSeq.HasValue, Is.False); + Assert.That (args.Index, Is.EqualTo (0)); + + args = new MessageFlagsChangedEventArgs (0, uid, MessageFlags.Answered, modseq); + Assert.That (args.Keywords, Is.Empty); + Assert.That (args.Flags, Is.EqualTo (MessageFlags.Answered)); + Assert.That (args.UniqueId, Is.EqualTo (uid)); + Assert.That (args.ModSeq, Is.EqualTo (modseq)); + Assert.That (args.Index, Is.EqualTo (0)); + + args = new MessageFlagsChangedEventArgs (0, uid, MessageFlags.Answered, keywords); + Assert.That (args.Keywords, Has.Count.EqualTo (keywords.Count)); + Assert.That (args.Flags, Is.EqualTo (MessageFlags.Answered)); + Assert.That (args.UniqueId, Is.EqualTo (uid)); + Assert.That (args.ModSeq.HasValue, Is.False); + Assert.That (args.Index, Is.EqualTo (0)); + + args = new MessageFlagsChangedEventArgs (0, uid, MessageFlags.Answered, keywords, modseq); + Assert.That (args.Keywords, Has.Count.EqualTo (keywords.Count)); + Assert.That (args.Flags, Is.EqualTo (MessageFlags.Answered)); + Assert.That (args.UniqueId, Is.EqualTo (uid)); + Assert.That (args.ModSeq, Is.EqualTo (modseq)); + Assert.That (args.Index, Is.EqualTo (0)); + + Assert.Throws (() => new MessageFlagsChangedEventArgs (-1)); + Assert.Throws (() => new MessageFlagsChangedEventArgs (-1, MessageFlags.Answered)); + Assert.Throws (() => new MessageFlagsChangedEventArgs (-1, MessageFlags.Answered, modseq)); + Assert.Throws (() => new MessageFlagsChangedEventArgs (-1, MessageFlags.Answered, keywords)); + Assert.Throws (() => new MessageFlagsChangedEventArgs (-1, MessageFlags.Answered, keywords, modseq)); + Assert.Throws (() => new MessageFlagsChangedEventArgs (-1, uid, MessageFlags.Answered)); + Assert.Throws (() => new MessageFlagsChangedEventArgs (-1, uid, MessageFlags.Answered, modseq)); + Assert.Throws (() => new MessageFlagsChangedEventArgs (-1, uid, MessageFlags.Answered, keywords)); + Assert.Throws (() => new MessageFlagsChangedEventArgs (-1, uid, MessageFlags.Answered, keywords, modseq)); + + Assert.Throws (() => new MessageFlagsChangedEventArgs (0, MessageFlags.Answered, null)); + Assert.Throws (() => new MessageFlagsChangedEventArgs (0, MessageFlags.Answered, null, modseq)); + Assert.Throws (() => new MessageFlagsChangedEventArgs (0, uid, MessageFlags.Answered, null)); + Assert.Throws (() => new MessageFlagsChangedEventArgs (0, uid, MessageFlags.Answered, null, modseq)); + } + + [Test] + public void TestMessageLabelsChangedEventArgs () + { + var labels = new string[] { "label1", "label2" }; + MessageLabelsChangedEventArgs args; + var uid = new UniqueId (5); + ulong modseq = 724; + + args = new MessageLabelsChangedEventArgs (0, labels); + Assert.That (args.Labels, Has.Count.EqualTo (labels.Length)); + Assert.That (args.UniqueId.HasValue, Is.False); + Assert.That (args.ModSeq.HasValue, Is.False); + Assert.That (args.Index, Is.EqualTo (0)); + + args = new MessageLabelsChangedEventArgs (0, labels, modseq); + Assert.That (args.Labels, Has.Count.EqualTo (labels.Length)); + Assert.That (args.UniqueId.HasValue, Is.False); + Assert.That (args.ModSeq, Is.EqualTo (modseq)); + Assert.That (args.Index, Is.EqualTo (0)); + + args = new MessageLabelsChangedEventArgs (0, uid, labels); + Assert.That (args.Labels, Has.Count.EqualTo (labels.Length)); + Assert.That (args.UniqueId, Is.EqualTo (uid)); + Assert.That (args.ModSeq.HasValue, Is.False); + Assert.That (args.Index, Is.EqualTo (0)); + + args = new MessageLabelsChangedEventArgs (0, uid, labels, modseq); + Assert.That (args.Labels, Has.Count.EqualTo (labels.Length)); + Assert.That (args.UniqueId, Is.EqualTo (uid)); + Assert.That (args.ModSeq, Is.EqualTo (modseq)); + Assert.That (args.Index, Is.EqualTo (0)); + + Assert.Throws (() => new MessageLabelsChangedEventArgs (-1, labels)); + Assert.Throws (() => new MessageLabelsChangedEventArgs (-1, labels, modseq)); + Assert.Throws (() => new MessageLabelsChangedEventArgs (-1, uid, labels)); + Assert.Throws (() => new MessageLabelsChangedEventArgs (-1, uid, labels, modseq)); + + Assert.Throws (() => new MessageLabelsChangedEventArgs (0, null)); + Assert.Throws (() => new MessageLabelsChangedEventArgs (0, null, modseq)); + Assert.Throws (() => new MessageLabelsChangedEventArgs (0, uid, null)); + Assert.Throws (() => new MessageLabelsChangedEventArgs (0, uid, null, modseq)); + } + + [Test] + public void TestMessageSentEventArgs () + { + var message = new MimeMessage (); + MessageSentEventArgs args; + + args = new MessageSentEventArgs (message, "response"); + + Assert.That (args.Message, Is.EqualTo (message)); + Assert.That (args.Response, Is.EqualTo ("response")); + + Assert.Throws (() => new MessageSentEventArgs (null, "response")); + Assert.Throws (() => new MessageSentEventArgs (message, null)); + } + + [Test] + public void TestMessageSummaryFetchedEventArgs () + { + var message = new MessageSummary (0); + MessageSummaryFetchedEventArgs args; + + args = new MessageSummaryFetchedEventArgs (message); + + Assert.That (args.Message, Is.EqualTo (message)); + + Assert.Throws (() => new MessageSummaryFetchedEventArgs (null)); + } + + [Test] + public void TestMessagesVanishedEventArgs () + { + var uids = new UniqueIdRange (0, 5, 7); + MessagesVanishedEventArgs args; + + args = new MessagesVanishedEventArgs (uids, true); + + Assert.That (args.UniqueIds, Is.EqualTo (uids)); + Assert.That (args.Earlier, Is.True); + + Assert.Throws (() => new MessagesVanishedEventArgs (null, false)); + } + + [Test] + public void TestMetadataChangedEventArgs () + { + var args = new MetadataChangedEventArgs (new Metadata (MetadataTag.PrivateComment, "this is a comment")); + + Assert.That (args.Metadata.Tag, Is.EqualTo (MetadataTag.PrivateComment), "Tag"); + Assert.That (args.Metadata.Value, Is.EqualTo ("this is a comment"), "Value"); + + Assert.Throws (() => new MetadataChangedEventArgs (null)); + } + + [Test] + public void TestModSeqChangedEventArgs () + { + ModSeqChangedEventArgs args; + ulong modseq = 724; + + args = new ModSeqChangedEventArgs (0, modseq); + Assert.That (args.UniqueId.HasValue, Is.False); + Assert.That (args.ModSeq, Is.EqualTo (modseq)); + Assert.That (args.Index, Is.EqualTo (0)); + + args = new ModSeqChangedEventArgs (0, UniqueId.MinValue, modseq); + Assert.That (args.UniqueId, Is.EqualTo (UniqueId.MinValue)); + Assert.That (args.ModSeq, Is.EqualTo (modseq)); + Assert.That (args.Index, Is.EqualTo (0)); + + Assert.Throws (() => new ModSeqChangedEventArgs (-1, modseq)); + Assert.Throws (() => new ModSeqChangedEventArgs (-1, UniqueId.MinValue, modseq)); + } + } +} diff --git a/UnitTests/ExceptionTests.cs b/UnitTests/ExceptionTests.cs index cdfad83b25..d0a9ad75fa 100644 --- a/UnitTests/ExceptionTests.cs +++ b/UnitTests/ExceptionTests.cs @@ -3,7 +3,7 @@ // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -24,21 +24,15 @@ // THE SOFTWARE. // -using System; -using System.IO; -using System.Runtime.Serialization.Formatters.Binary; +#if NET6_0 -using NUnit.Framework; +using System.Runtime.Serialization.Formatters.Binary; -using MimeKit; using MailKit; using MailKit.Net.Imap; using MailKit.Net.Pop3; -using MailKit.Net.Smtp; -using MailKit.Security; -namespace UnitTests -{ +namespace UnitTests { [TestFixture] public class ExceptionTests { @@ -53,8 +47,36 @@ public void TestFolderNotFoundException () stream.Position = 0; var ex = (FolderNotFoundException) formatter.Deserialize (stream); - Assert.AreEqual (expected.FolderName, ex.FolderName, "Unexpected FolderName."); + Assert.That (ex.FolderName, Is.EqualTo (expected.FolderName), "Unexpected FolderName."); + } + + expected = new FolderNotFoundException ("This is the error message.", "Inbox"); + + using (var stream = new MemoryStream ()) { + var formatter = new BinaryFormatter (); + formatter.Serialize (stream, expected); + stream.Position = 0; + + var ex = (FolderNotFoundException) formatter.Deserialize (stream); + Assert.That (ex.Message, Is.EqualTo (expected.Message), "Unexpected Message."); + Assert.That (ex.FolderName, Is.EqualTo (expected.FolderName), "Unexpected FolderName."); + } + + expected = new FolderNotFoundException ("This is the error message.", "Inbox", new IOException ("Inner Exception")); + + using (var stream = new MemoryStream ()) { + var formatter = new BinaryFormatter (); + formatter.Serialize (stream, expected); + stream.Position = 0; + + var ex = (FolderNotFoundException) formatter.Deserialize (stream); + Assert.That (ex.Message, Is.EqualTo (expected.Message), "Unexpected Message."); + Assert.That (ex.FolderName, Is.EqualTo (expected.FolderName), "Unexpected FolderName."); } + + Assert.Throws (() => new FolderNotFoundException (null)); + Assert.Throws (() => new FolderNotFoundException ("message", null)); + Assert.Throws (() => new FolderNotFoundException ("message", null, new Exception ("message"))); } [Test] @@ -68,193 +90,259 @@ public void TestFolderNotOpenException () stream.Position = 0; var ex = (FolderNotOpenException) formatter.Deserialize (stream); - Assert.AreEqual (expected.FolderName, ex.FolderName, "Unexpected FolderName."); - Assert.AreEqual (expected.FolderAccess, ex.FolderAccess, "Unexpected FolderAcess."); + Assert.That (ex.FolderName, Is.EqualTo (expected.FolderName), "Unexpected FolderName."); + Assert.That (ex.FolderAccess, Is.EqualTo (expected.FolderAccess), "Unexpected FolderAccess."); + } + + expected = new FolderNotOpenException ("Inbox", FolderAccess.ReadWrite, "This is the error message."); + + using (var stream = new MemoryStream ()) { + var formatter = new BinaryFormatter (); + formatter.Serialize (stream, expected); + stream.Position = 0; + + var ex = (FolderNotOpenException) formatter.Deserialize (stream); + Assert.That (ex.FolderName, Is.EqualTo (expected.FolderName), "Unexpected FolderName."); + Assert.That (ex.FolderAccess, Is.EqualTo (expected.FolderAccess), "Unexpected FolderAccess."); } + + expected = new FolderNotOpenException ("Inbox", FolderAccess.ReadWrite, "This is the error message.", new IOException ("Inner Exception")); + + using (var stream = new MemoryStream ()) { + var formatter = new BinaryFormatter (); + formatter.Serialize (stream, expected); + stream.Position = 0; + + var ex = (FolderNotOpenException) formatter.Deserialize (stream); + Assert.That (ex.FolderName, Is.EqualTo (expected.FolderName), "Unexpected FolderName."); + Assert.That (ex.FolderAccess, Is.EqualTo (expected.FolderAccess), "Unexpected FolderAccess."); + } + + Assert.Throws (() => new FolderNotOpenException (null, FolderAccess.ReadOnly)); + Assert.Throws (() => new FolderNotOpenException (null, FolderAccess.ReadOnly, "message")); + Assert.Throws (() => new FolderNotOpenException (null, FolderAccess.ReadOnly, "message", new Exception ("message"))); } [Test] - public void TestImapCommandException () + public void TestMessageNotFoundException () { - var expected = new ImapCommandException (ImapCommandResponse.Bad, "Bad boys, bad boys. Whatcha gonna do?", "Message", new Exception ("InnerException")); + var expected = new MessageNotFoundException ("This is the message."); using (var stream = new MemoryStream ()) { var formatter = new BinaryFormatter (); formatter.Serialize (stream, expected); stream.Position = 0; - var ex = (ImapCommandException)formatter.Deserialize (stream); - Assert.AreEqual (expected.Response, ex.Response, "Unexpected Response."); - Assert.AreEqual (expected.ResponseText, ex.ResponseText, "Unexpected ResponseText."); + var ex = (MessageNotFoundException) formatter.Deserialize (stream); + Assert.That (ex.Message, Is.EqualTo (expected.Message), "Unexpected Message."); } - expected = new ImapCommandException (ImapCommandResponse.Bad, "Bad boys, bad boys. Whatcha gonna do?", "Message"); + expected = new MessageNotFoundException ("This is the message.", new IOException ("Inner Exception")); using (var stream = new MemoryStream ()) { var formatter = new BinaryFormatter (); formatter.Serialize (stream, expected); stream.Position = 0; - var ex = (ImapCommandException) formatter.Deserialize (stream); - Assert.AreEqual (expected.Response, ex.Response, "Unexpected Response."); - Assert.AreEqual (expected.ResponseText, ex.ResponseText, "Unexpected ResponseText."); + var ex = (MessageNotFoundException)formatter.Deserialize (stream); + Assert.That (ex.Message, Is.EqualTo (expected.Message), "Unexpected Message."); } } [Test] - public void TestImapProtocolException () + public void TestServiceNotAuthenticatedException () { - var expected = new ImapProtocolException ("Bad boys, bad boys. Whatcha gonna do?", new Exception ("InnerException")); + var expected = new ServiceNotAuthenticatedException (); using (var stream = new MemoryStream ()) { var formatter = new BinaryFormatter (); formatter.Serialize (stream, expected); stream.Position = 0; - var ex = (ImapProtocolException) formatter.Deserialize (stream); - Assert.AreEqual (expected.HelpLink, ex.HelpLink, "Unexpected HelpLink."); + var ex = (ServiceNotAuthenticatedException) formatter.Deserialize (stream); + Assert.That (ex.Message, Is.EqualTo (expected.Message), "Unexpected Message."); } - expected = new ImapProtocolException ("Bad boys, bad boys. Whatcha gonna do?"); + expected = new ServiceNotAuthenticatedException ("This is the message."); using (var stream = new MemoryStream ()) { var formatter = new BinaryFormatter (); formatter.Serialize (stream, expected); stream.Position = 0; - var ex = (ImapProtocolException) formatter.Deserialize (stream); - Assert.AreEqual (expected.HelpLink, ex.HelpLink, "Unexpected HelpLink."); + var ex = (ServiceNotAuthenticatedException) formatter.Deserialize (stream); + Assert.That (ex.Message, Is.EqualTo (expected.Message), "Unexpected Message."); } - expected = new ImapProtocolException (); + expected = new ServiceNotAuthenticatedException ("This is the message.", new IOException ("Inner Exception")); using (var stream = new MemoryStream ()) { var formatter = new BinaryFormatter (); formatter.Serialize (stream, expected); stream.Position = 0; - var ex = (ImapProtocolException) formatter.Deserialize (stream); - Assert.AreEqual (expected.HelpLink, ex.HelpLink, "Unexpected HelpLink."); + var ex = (ServiceNotAuthenticatedException) formatter.Deserialize (stream); + Assert.That (ex.Message, Is.EqualTo (expected.Message), "Unexpected Message."); } } [Test] - public void TestPop3CommandException () + public void TestServiceNotConnectedException () { - var expected = new Pop3CommandException ("Message", "Bad boys, bad boys. Whatcha gonna do?"); + var expected = new ServiceNotConnectedException (); - Assert.Throws (() => new Pop3CommandException ("Message", (string) null)); - Assert.Throws (() => new Pop3CommandException ("Message", null, new Exception ("inner"))); + using (var stream = new MemoryStream ()) { + var formatter = new BinaryFormatter (); + formatter.Serialize (stream, expected); + stream.Position = 0; + + var ex = (ServiceNotConnectedException) formatter.Deserialize (stream); + Assert.That (ex.Message, Is.EqualTo (expected.Message), "Unexpected Message."); + } + + expected = new ServiceNotConnectedException ("This is the message."); using (var stream = new MemoryStream ()) { var formatter = new BinaryFormatter (); formatter.Serialize (stream, expected); stream.Position = 0; - var ex = (Pop3CommandException) formatter.Deserialize (stream); - Assert.AreEqual (expected.StatusText, ex.StatusText, "Unexpected StatusText."); + var ex = (ServiceNotConnectedException) formatter.Deserialize (stream); + Assert.That (ex.Message, Is.EqualTo (expected.Message), "Unexpected Message."); + } + + expected = new ServiceNotConnectedException ("This is the message.", new IOException ("Inner Exception")); + + using (var stream = new MemoryStream ()) { + var formatter = new BinaryFormatter (); + formatter.Serialize (stream, expected); + stream.Position = 0; + + var ex = (ServiceNotConnectedException) formatter.Deserialize (stream); + Assert.That (ex.Message, Is.EqualTo (expected.Message), "Unexpected Message."); } } [Test] - public void TestPop3ProtocolException () + public void TestImapCommandException () { - var expected = new Pop3ProtocolException ("Bad boys, bad boys. Whatcha gonna do?", new Exception ("InnerException")); + var expected = new ImapCommandException (ImapCommandResponse.Bad, "Bad boys, bad boys. Whatcha gonna do?", "Message", new Exception ("InnerException")); using (var stream = new MemoryStream ()) { var formatter = new BinaryFormatter (); formatter.Serialize (stream, expected); stream.Position = 0; - var ex = (Pop3ProtocolException) formatter.Deserialize (stream); - Assert.AreEqual (expected.HelpLink, ex.HelpLink, "Unexpected HelpLink."); + var ex = (ImapCommandException)formatter.Deserialize (stream); + Assert.That (ex.Response, Is.EqualTo (expected.Response), "Unexpected Response."); + Assert.That (ex.ResponseText, Is.EqualTo (expected.ResponseText), "Unexpected ResponseText."); } - expected = new Pop3ProtocolException ("Bad boys, bad boys. Whatcha gonna do?"); + expected = new ImapCommandException (ImapCommandResponse.Bad, "Bad boys, bad boys. Whatcha gonna do?", "Message"); using (var stream = new MemoryStream ()) { var formatter = new BinaryFormatter (); formatter.Serialize (stream, expected); stream.Position = 0; - var ex = (Pop3ProtocolException) formatter.Deserialize (stream); - Assert.AreEqual (expected.HelpLink, ex.HelpLink, "Unexpected HelpLink."); + var ex = (ImapCommandException) formatter.Deserialize (stream); + Assert.That (ex.Response, Is.EqualTo (expected.Response), "Unexpected Response."); + Assert.That (ex.ResponseText, Is.EqualTo (expected.ResponseText), "Unexpected ResponseText."); } + } - expected = new Pop3ProtocolException (); + [Test] + public void TestImapProtocolException () + { + var expected = new ImapProtocolException ("Bad boys, bad boys. Whatcha gonna do?", new Exception ("InnerException")); using (var stream = new MemoryStream ()) { var formatter = new BinaryFormatter (); formatter.Serialize (stream, expected); stream.Position = 0; - var ex = (Pop3ProtocolException) formatter.Deserialize (stream); - Assert.AreEqual (expected.HelpLink, ex.HelpLink, "Unexpected HelpLink."); + var ex = (ImapProtocolException) formatter.Deserialize (stream); + Assert.That (ex.HelpLink, Is.EqualTo (expected.HelpLink), "Unexpected HelpLink."); } - } - static void TestSmtpCommandException (SmtpCommandException expected) - { + expected = new ImapProtocolException ("Bad boys, bad boys. Whatcha gonna do?"); + using (var stream = new MemoryStream ()) { var formatter = new BinaryFormatter (); formatter.Serialize (stream, expected); stream.Position = 0; - var ex = (SmtpCommandException) formatter.Deserialize (stream); - Assert.AreEqual (expected.ErrorCode, ex.ErrorCode, "Unexpected ErrorCode."); - Assert.AreEqual (expected.StatusCode, ex.StatusCode, "Unexpected StatusCode."); + var ex = (ImapProtocolException) formatter.Deserialize (stream); + Assert.That (ex.HelpLink, Is.EqualTo (expected.HelpLink), "Unexpected HelpLink."); + } + + expected = new ImapProtocolException (); + + using (var stream = new MemoryStream ()) { + var formatter = new BinaryFormatter (); + formatter.Serialize (stream, expected); + stream.Position = 0; - if (expected.Mailbox != null) - Assert.IsTrue (expected.Mailbox.Equals (ex.Mailbox), "Unexpected Mailbox."); - else - Assert.IsNull (ex.Mailbox, "Expected Mailbox to be null."); + var ex = (ImapProtocolException) formatter.Deserialize (stream); + Assert.That (ex.HelpLink, Is.EqualTo (expected.HelpLink), "Unexpected HelpLink."); } } [Test] - public void TestSmtpCommandException () + public void TestPop3CommandException () { - TestSmtpCommandException (new SmtpCommandException (SmtpErrorCode.RecipientNotAccepted, SmtpStatusCode.MailboxUnavailable, - new MailboxAddress ("Unit Tests", "example@mimekit.net"), "Message")); - TestSmtpCommandException (new SmtpCommandException (SmtpErrorCode.MessageNotAccepted, SmtpStatusCode.InsufficientStorage, - "Message")); + var expected = new Pop3CommandException ("Message", "Bad boys, bad boys. Whatcha gonna do?"); + + Assert.Throws (() => new Pop3CommandException ("Message", (string) null)); + Assert.Throws (() => new Pop3CommandException ("Message", null, new Exception ("inner"))); + + using (var stream = new MemoryStream ()) { + var formatter = new BinaryFormatter (); + formatter.Serialize (stream, expected); + stream.Position = 0; + + var ex = (Pop3CommandException) formatter.Deserialize (stream); + Assert.That (ex.StatusText, Is.EqualTo (expected.StatusText), "Unexpected StatusText."); + } } [Test] - public void TestSmtpProtocolException () + public void TestPop3ProtocolException () { - var expected = new SmtpProtocolException ("Bad boys, bad boys. Whatcha gonna do?", new Exception ("InnerException")); + var expected = new Pop3ProtocolException ("Bad boys, bad boys. Whatcha gonna do?", new Exception ("InnerException")); using (var stream = new MemoryStream ()) { var formatter = new BinaryFormatter (); formatter.Serialize (stream, expected); stream.Position = 0; - var ex = (SmtpProtocolException) formatter.Deserialize (stream); - Assert.AreEqual (expected.HelpLink, ex.HelpLink, "Unexpected HelpLink."); + var ex = (Pop3ProtocolException) formatter.Deserialize (stream); + Assert.That (ex.HelpLink, Is.EqualTo (expected.HelpLink), "Unexpected HelpLink."); } - expected = new SmtpProtocolException ("Bad boys, bad boys. Whatcha gonna do?"); + expected = new Pop3ProtocolException ("Bad boys, bad boys. Whatcha gonna do?"); using (var stream = new MemoryStream ()) { var formatter = new BinaryFormatter (); formatter.Serialize (stream, expected); stream.Position = 0; - var ex = (SmtpProtocolException) formatter.Deserialize (stream); - Assert.AreEqual (expected.HelpLink, ex.HelpLink, "Unexpected HelpLink."); + var ex = (Pop3ProtocolException) formatter.Deserialize (stream); + Assert.That (ex.HelpLink, Is.EqualTo (expected.HelpLink), "Unexpected HelpLink."); } - expected = new SmtpProtocolException (); + expected = new Pop3ProtocolException (); using (var stream = new MemoryStream ()) { var formatter = new BinaryFormatter (); formatter.Serialize (stream, expected); stream.Position = 0; - var ex = (SmtpProtocolException) formatter.Deserialize (stream); - Assert.AreEqual (expected.HelpLink, ex.HelpLink, "Unexpected HelpLink."); + var ex = (Pop3ProtocolException) formatter.Deserialize (stream); + Assert.That (ex.HelpLink, Is.EqualTo (expected.HelpLink), "Unexpected HelpLink."); } } } } + +#endif // NET6_0 diff --git a/UnitTests/ExceptionalProtocolLogger.cs b/UnitTests/ExceptionalProtocolLogger.cs new file mode 100644 index 0000000000..cf8c0fa987 --- /dev/null +++ b/UnitTests/ExceptionalProtocolLogger.cs @@ -0,0 +1,70 @@ +// +// ExceptionalProtocolLogger.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using MailKit; + +namespace UnitTests { + enum ExceptionalProtocolLoggerMode + { + ThrowOnLogConnect, + ThrowOnLogClient, + ThrowOnLogServer, + } + + class ExceptionalProtocolLogger : IProtocolLogger + { + readonly ExceptionalProtocolLoggerMode mode; + + public IAuthenticationSecretDetector AuthenticationSecretDetector { get; set; } + + public ExceptionalProtocolLogger (ExceptionalProtocolLoggerMode mode) + { + this.mode = mode; + } + + public void LogConnect (Uri uri) + { + if (mode == ExceptionalProtocolLoggerMode.ThrowOnLogConnect) + throw new NotImplementedException (); + } + + public void LogClient (byte[] buffer, int offset, int count) + { + if (mode == ExceptionalProtocolLoggerMode.ThrowOnLogClient) + throw new NotImplementedException (); + } + + public void LogServer (byte[] buffer, int offset, int count) + { + if (mode == ExceptionalProtocolLoggerMode.ThrowOnLogServer) + throw new NotImplementedException (); + } + + public void Dispose () + { + } + } +} diff --git a/UnitTests/FolderNamespaceTests.cs b/UnitTests/FolderNamespaceTests.cs new file mode 100644 index 0000000000..c2beacdf39 --- /dev/null +++ b/UnitTests/FolderNamespaceTests.cs @@ -0,0 +1,82 @@ +// +// FolderNamespaceTests.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System.Collections; + +using MailKit; + +namespace UnitTests { + [TestFixture] + public class FolderNamespaceTests + { + [Test] + public void TestFolderNamespace () + { + Assert.Throws (() => new FolderNamespace ('.', null)); + } + + [Test] + public void TestFolderNamespaceCollection () + { + var namespaces = new FolderNamespaceCollection (); + FolderNamespace ns; + int i = 0; + + Assert.Throws (() => namespaces.Add (null)); + Assert.Throws (() => namespaces.Contains (null)); + Assert.Throws (() => namespaces.Remove (null)); + Assert.Throws (() => ns = namespaces[-1]); + Assert.Throws (() => namespaces[-1] = new FolderNamespace ('.', "")); + + Assert.That (namespaces, Is.Empty); + + ns = new FolderNamespace ('.', ""); + namespaces.Add (ns); + Assert.That (namespaces, Has.Count.EqualTo (1)); + Assert.That (namespaces.Contains (ns), Is.True); + Assert.Throws (() => namespaces[0] = null); + + ns = new FolderNamespace ('\\', ""); + namespaces[0] = ns; + Assert.That (namespaces, Has.Count.EqualTo (1)); + Assert.That (namespaces.Contains (ns), Is.True); + + Assert.That (namespaces.Remove (ns), Is.True); + Assert.That (namespaces, Is.Empty); + Assert.That (namespaces.Contains (ns), Is.False); + + namespaces.Add (new FolderNamespace ('.', "")); + namespaces.Add (new FolderNamespace ('\\', "")); + foreach (var item in namespaces) + Assert.That (item, Is.EqualTo (namespaces[i++])); + i = 0; + foreach (object item in (IEnumerable) namespaces) + Assert.That (item, Is.EqualTo (namespaces[i++])); + + Assert.That (namespaces.ToString (), Is.EqualTo ("((\".\" \"\")(\"\\\\\" \"\"))")); + } + } +} diff --git a/UnitTests/HeaderSetTests.cs b/UnitTests/HeaderSetTests.cs new file mode 100644 index 0000000000..2906bb1f1a --- /dev/null +++ b/UnitTests/HeaderSetTests.cs @@ -0,0 +1,113 @@ +// +// HeaderSetTests.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System.Collections; + +using MimeKit; +using MailKit; + +namespace UnitTests { + [TestFixture] + public class HeaderSetTests + { + [Test] + public void TestArgumentExceptions () + { + var headers = new HeaderSet (); + var array = new string[10]; + + Assert.Throws (() => headers.Add (HeaderId.Unknown)); + Assert.Throws (() => headers.AddRange ((IEnumerable) null)); + Assert.Throws (() => headers.AddRange (new HeaderId[] { HeaderId.Unknown })); + + Assert.Throws (() => headers.Add (null)); + Assert.Throws (() => headers.Add (string.Empty)); + Assert.Throws (() => headers.Add ("This is invalid")); + Assert.Throws (() => headers.AddRange ((IEnumerable) null)); + Assert.Throws (() => headers.AddRange (new string[] { "This is invalid" })); + + Assert.Throws (() => ((ICollection) headers).Add (null)); + Assert.Throws (() => ((ICollection) headers).Add (string.Empty)); + Assert.Throws (() => ((ICollection) headers).Add ("This is invalid")); + + Assert.Throws (() => headers.CopyTo (null, 0)); + Assert.Throws (() => headers.CopyTo (array, -1)); + Assert.Throws (() => headers.CopyTo (array, 11)); + + Assert.Throws (() => headers.Contains (HeaderId.Unknown)); + Assert.Throws (() => headers.Contains (null)); + + Assert.Throws (() => headers.Remove (HeaderId.Unknown)); + Assert.Throws (() => headers.Remove (null)); + } + + [Test] + public void TestBasicFunctionality () + { + var headers = new HeaderSet (); + + Assert.That (headers.Add ("From"), Is.True, "Adding From"); + Assert.That (headers.Add ("From"), Is.False, "Adding From duplicate #1"); + Assert.That (headers.Add ("FROM"), Is.False, "Adding From duplicate #2"); + Assert.That (headers.Add ("fRoM"), Is.False, "Adding From duplicate #3"); + Assert.That (headers.Add (HeaderId.From), Is.False, "Adding From duplicate #4"); + Assert.That (headers, Has.Count.EqualTo (1), "Count #1"); + + Assert.That (headers.Remove (HeaderId.From), Is.True, "Removing From"); + Assert.That (headers.Remove ("From"), Is.False, "Removing From duplicate #1"); + Assert.That (headers.Remove (HeaderId.From), Is.False, "Removing From duplicate #2"); + Assert.That (headers, Is.Empty, "Count #2"); + + headers.AddRange (new HeaderId[] { HeaderId.Sender, HeaderId.From, HeaderId.ReplyTo }); + Assert.That (headers, Has.Count.EqualTo (3), "Count #3"); + + headers.AddRange (new string[] { "to", "cc", "bcc" }); + Assert.That (headers, Has.Count.EqualTo (6), "Count #4"); + + Assert.That (headers.Contains (HeaderId.To), Is.True, "Contains #1"); + Assert.That (headers.Contains ("reply-to"), Is.True, "Contains #2"); + + var results = new string[headers.Count]; + headers.CopyTo (results, 0); + Array.Sort (results); + Assert.That (results[0], Is.EqualTo ("BCC")); + Assert.That (results[1], Is.EqualTo ("CC")); + Assert.That (results[2], Is.EqualTo ("FROM")); + Assert.That (results[3], Is.EqualTo ("REPLY-TO")); + Assert.That (results[4], Is.EqualTo ("SENDER")); + Assert.That (results[5], Is.EqualTo ("TO")); + + foreach (var header in headers) + Assert.That (results, Does.Contain (header)); + + foreach (string header in ((IEnumerable) headers)) + Assert.That (results, Does.Contain (header)); + + headers.Clear (); + Assert.That (headers, Is.Empty, "Count after Clear"); + } + } +} diff --git a/UnitTests/MailServiceTests.cs b/UnitTests/MailServiceTests.cs new file mode 100644 index 0000000000..78a8f5441a --- /dev/null +++ b/UnitTests/MailServiceTests.cs @@ -0,0 +1,85 @@ +// +// MailServiceTests.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; + +using MailKit; +using MailKit.Security; +using MailKit.Net.Imap; +using MailKit.Net.Pop3; +using MailKit.Net.Smtp; + +namespace UnitTests { + [TestFixture] + public class MailServiceTests + { + bool SslCertificateValidationCallback (object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) + { + var certificate2 = certificate as X509Certificate2; + + Assert.That (certificate2, Is.Not.Null, "Cast"); + + return true; + } + + [Test] + public void TestIsKnownMailServerCertificate () + { + var servers = new string[] { + "imap://imap.gmail.com:993", + "pop://pop.gmail.com:995", + "smtp://smtp.gmail.com:587", + + "imap://outlook.office365.com:993", + "pop://outlook.office365.com:995", + "smtp://smtp.office365.com:587", + }; + + foreach (var server in servers) { + var uri = new Uri (server); + MailService client; + + switch (uri.Scheme) { + case "imap": client = new ImapClient (); break; + case "pop": client = new Pop3Client (); break; + case "smtp": client = new SmtpClient (); break; + default: throw new Exception ("Unsupported protocol"); + } + + using (client) { + client.ServerCertificateValidationCallback = SslCertificateValidationCallback; + try { + client.Connect (uri.Host, uri.Port, SecureSocketOptions.Auto); + } catch { + continue; + } + client.Disconnect (true); + } + } + } + } +} diff --git a/UnitTests/MessageSortingTests.cs b/UnitTests/MessageSortingTests.cs index 8a4b56b380..d18d2dc9ba 100644 --- a/UnitTests/MessageSortingTests.cs +++ b/UnitTests/MessageSortingTests.cs @@ -1,9 +1,9 @@ -// +// // MessageSortingTests.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -24,11 +24,6 @@ // THE SOFTWARE. // -using System; -using System.Collections.Generic; - -using NUnit.Framework; - using MimeKit; using MailKit.Search; @@ -43,7 +38,7 @@ public void TestArgumentExceptions () { var messages = new List { new MessageSummary (0) }; var orderBy = new OrderBy[] { OrderBy.Subject }; - var emptyOrderBy = new OrderBy[0]; + var emptyOrderBy = Array.Empty (); Assert.Throws (() => MessageSorter.Sort ((List) null, orderBy)); Assert.Throws (() => MessageSorter.Sort (messages, null)); @@ -56,109 +51,234 @@ public void TestArgumentExceptions () Assert.Throws (() => MessageSorter.Sort ((IEnumerable) messages, orderBy)); } - [Test] - public void TestSorting () + static List Create () { var messages = new List (); MessageSummary summary; summary = new MessageSummary (0); - summary.Fields = MessageSummaryItems.Envelope | MessageSummaryItems.Size; + summary.Fields = MessageSummaryItems.Annotations | MessageSummaryItems.Envelope | MessageSummaryItems.Size | MessageSummaryItems.ModSeq; + summary.Annotations = new List (new [] { + new Annotation (AnnotationEntry.AltSubject) + }); + summary.Annotations[0].Properties.Add (AnnotationAttribute.SharedValue, "aaaa"); summary.Envelope = new Envelope (); summary.Envelope.Date = DateTimeOffset.Now.AddSeconds (-2); summary.Envelope.Subject = "aaaa"; summary.Envelope.From.Add (new MailboxAddress ("A", "a@a.com")); summary.Envelope.To.Add (new MailboxAddress ("A", "a@a.com")); summary.Envelope.Cc.Add (new MailboxAddress ("A", "a@a.com")); + summary.ModSeq = 80290; summary.Size = 520; messages.Add (summary); summary = new MessageSummary (1); - summary.Fields = MessageSummaryItems.Envelope | MessageSummaryItems.Size; + summary.Fields = MessageSummaryItems.Annotations | MessageSummaryItems.Envelope | MessageSummaryItems.Size | MessageSummaryItems.ModSeq; + summary.Annotations = new List (new [] { + new Annotation (AnnotationEntry.AltSubject) + }); + summary.Annotations[0].Properties.Add (AnnotationAttribute.SharedValue, "bbbb"); summary.Envelope = new Envelope (); summary.Envelope.Date = DateTimeOffset.Now.AddSeconds (-1); summary.Envelope.Subject = "bbbb"; summary.Envelope.From.Add (new MailboxAddress ("B", "b@b.com")); summary.Envelope.To.Add (new MailboxAddress ("B", "b@b.com")); summary.Envelope.Cc.Add (new MailboxAddress ("B", "b@b.com")); + summary.ModSeq = 70642; summary.Size = 265; messages.Add (summary); summary = new MessageSummary (2); - summary.Fields = MessageSummaryItems.Envelope | MessageSummaryItems.Size; + summary.Fields = MessageSummaryItems.Annotations | MessageSummaryItems.Envelope | MessageSummaryItems.Size | MessageSummaryItems.ModSeq; + summary.Annotations = new List (new [] { + new Annotation (AnnotationEntry.AltSubject) + }); + summary.Annotations[0].Properties.Add (AnnotationAttribute.SharedValue, "cccc"); summary.Envelope = new Envelope (); summary.Envelope.Date = DateTimeOffset.Now; summary.Envelope.Subject = "cccc"; summary.Envelope.From.Add (new MailboxAddress ("C", "c@c.com")); summary.Envelope.To.Add (new MailboxAddress ("C", "c@c.com")); summary.Envelope.Cc.Add (new MailboxAddress ("C", "c@c.com")); + summary.ModSeq = 80290; summary.Size = 520; messages.Add (summary); + return messages; + } + + [Test] + public void TestSorting () + { + var messages = Create (); + messages.Sort (new[] { OrderBy.Arrival }); - Assert.AreEqual (0, messages[0].Index, "Sorting by arrival failed."); - Assert.AreEqual (1, messages[1].Index, "Sorting by arrival failed."); - Assert.AreEqual (2, messages[2].Index, "Sorting by arrival failed."); + Assert.That (messages[0].Index, Is.EqualTo (0), "Sorting by arrival failed."); + Assert.That (messages[1].Index, Is.EqualTo (1), "Sorting by arrival failed."); + Assert.That (messages[2].Index, Is.EqualTo (2), "Sorting by arrival failed."); messages.Sort (new [] { OrderBy.ReverseArrival }); - Assert.AreEqual (2, messages[0].Index, "Sorting by reverse arrival failed."); - Assert.AreEqual (1, messages[1].Index, "Sorting by reverse arrival failed."); - Assert.AreEqual (0, messages[2].Index, "Sorting by reverse arrival failed."); + Assert.That (messages[0].Index, Is.EqualTo (2), "Sorting by reverse arrival failed."); + Assert.That (messages[1].Index, Is.EqualTo (1), "Sorting by reverse arrival failed."); + Assert.That (messages[2].Index, Is.EqualTo (0), "Sorting by reverse arrival failed."); messages.Sort (new [] { OrderBy.Subject }); - Assert.AreEqual (0, messages[0].Index, "Sorting by subject failed."); - Assert.AreEqual (1, messages[1].Index, "Sorting by subject failed."); - Assert.AreEqual (2, messages[2].Index, "Sorting by subject failed."); + Assert.That (messages[0].Index, Is.EqualTo (0), "Sorting by subject failed."); + Assert.That (messages[1].Index, Is.EqualTo (1), "Sorting by subject failed."); + Assert.That (messages[2].Index, Is.EqualTo (2), "Sorting by subject failed."); messages.Sort (new [] { OrderBy.ReverseSubject }); - Assert.AreEqual (2, messages[0].Index, "Sorting by reverse subject failed."); - Assert.AreEqual (1, messages[1].Index, "Sorting by reverse subject failed."); - Assert.AreEqual (0, messages[2].Index, "Sorting by reverse subject failed."); + Assert.That (messages[0].Index, Is.EqualTo (2), "Sorting by reverse subject failed."); + Assert.That (messages[1].Index, Is.EqualTo (1), "Sorting by reverse subject failed."); + Assert.That (messages[2].Index, Is.EqualTo (0), "Sorting by reverse subject failed."); messages.Sort (new [] { OrderBy.Size, OrderBy.Arrival }); - Assert.AreEqual (1, messages[0].Index, "Sorting by size failed."); - Assert.AreEqual (0, messages[1].Index, "Sorting by size failed."); - Assert.AreEqual (2, messages[2].Index, "Sorting by size failed."); + Assert.That (messages[0].Index, Is.EqualTo (1), "Sorting by size failed."); + Assert.That (messages[1].Index, Is.EqualTo (0), "Sorting by size failed."); + Assert.That (messages[2].Index, Is.EqualTo (2), "Sorting by size failed."); messages.Sort (new [] { OrderBy.Date }); - Assert.AreEqual (0, messages[0].Index, "Sorting by date failed."); - Assert.AreEqual (1, messages[1].Index, "Sorting by date failed."); - Assert.AreEqual (2, messages[2].Index, "Sorting by date failed."); + Assert.That (messages[0].Index, Is.EqualTo (0), "Sorting by date failed."); + Assert.That (messages[1].Index, Is.EqualTo (1), "Sorting by date failed."); + Assert.That (messages[2].Index, Is.EqualTo (2), "Sorting by date failed."); messages.Sort (new [] { OrderBy.Size, OrderBy.Subject }); - Assert.AreEqual (1, messages[0].Index, "Sorting by size+subject failed."); - Assert.AreEqual (0, messages[1].Index, "Sorting by size+subject failed."); - Assert.AreEqual (2, messages[2].Index, "Sorting by size+subject failed."); + Assert.That (messages[0].Index, Is.EqualTo (1), "Sorting by size+subject failed."); + Assert.That (messages[1].Index, Is.EqualTo (0), "Sorting by size+subject failed."); + Assert.That (messages[2].Index, Is.EqualTo (2), "Sorting by size+subject failed."); messages.Sort (new [] { OrderBy.ReverseSize, OrderBy.ReverseSubject }); - Assert.AreEqual (2, messages[0].Index, "Sorting by reversed size+subject failed."); - Assert.AreEqual (0, messages[1].Index, "Sorting by reversed size+subject failed."); - Assert.AreEqual (1, messages[2].Index, "Sorting by reversed size+subject failed."); + Assert.That (messages[0].Index, Is.EqualTo (2), "Sorting by reversed size+subject failed."); + Assert.That (messages[1].Index, Is.EqualTo (0), "Sorting by reversed size+subject failed."); + Assert.That (messages[2].Index, Is.EqualTo (1), "Sorting by reversed size+subject failed."); messages.Sort (new[] { OrderBy.DisplayFrom }); - Assert.AreEqual (0, messages[0].Index, "Sorting by display-from failed."); - Assert.AreEqual (1, messages[1].Index, "Sorting by display-from failed."); - Assert.AreEqual (2, messages[2].Index, "Sorting by display-from failed."); + Assert.That (messages[0].Index, Is.EqualTo (0), "Sorting by display-from failed."); + Assert.That (messages[1].Index, Is.EqualTo (1), "Sorting by display-from failed."); + Assert.That (messages[2].Index, Is.EqualTo (2), "Sorting by display-from failed."); messages.Sort (new[] { OrderBy.From }); - Assert.AreEqual (0, messages[0].Index, "Sorting by from failed."); - Assert.AreEqual (1, messages[1].Index, "Sorting by from failed."); - Assert.AreEqual (2, messages[2].Index, "Sorting by from failed."); + Assert.That (messages[0].Index, Is.EqualTo (0), "Sorting by from failed."); + Assert.That (messages[1].Index, Is.EqualTo (1), "Sorting by from failed."); + Assert.That (messages[2].Index, Is.EqualTo (2), "Sorting by from failed."); messages.Sort (new[] { OrderBy.DisplayTo }); - Assert.AreEqual (0, messages[0].Index, "Sorting by display-to failed."); - Assert.AreEqual (1, messages[1].Index, "Sorting by display-to failed."); - Assert.AreEqual (2, messages[2].Index, "Sorting by display-to failed."); + Assert.That (messages[0].Index, Is.EqualTo (0), "Sorting by display-to failed."); + Assert.That (messages[1].Index, Is.EqualTo (1), "Sorting by display-to failed."); + Assert.That (messages[2].Index, Is.EqualTo (2), "Sorting by display-to failed."); messages.Sort (new[] { OrderBy.To }); - Assert.AreEqual (0, messages[0].Index, "Sorting by to failed."); - Assert.AreEqual (1, messages[1].Index, "Sorting by to failed."); - Assert.AreEqual (2, messages[2].Index, "Sorting by to failed."); + Assert.That (messages[0].Index, Is.EqualTo (0), "Sorting by to failed."); + Assert.That (messages[1].Index, Is.EqualTo (1), "Sorting by to failed."); + Assert.That (messages[2].Index, Is.EqualTo (2), "Sorting by to failed."); messages.Sort (new[] { OrderBy.Cc }); - Assert.AreEqual (0, messages[0].Index, "Sorting by cc failed."); - Assert.AreEqual (1, messages[1].Index, "Sorting by cc failed."); - Assert.AreEqual (2, messages[2].Index, "Sorting by cc failed."); + Assert.That (messages[0].Index, Is.EqualTo (0), "Sorting by cc failed."); + Assert.That (messages[1].Index, Is.EqualTo (1), "Sorting by cc failed."); + Assert.That (messages[2].Index, Is.EqualTo (2), "Sorting by cc failed."); + + messages.Sort (new [] { new OrderBy (OrderByType.ModSeq, SortOrder.Ascending), OrderBy.Arrival }); + Assert.That (messages[0].Index, Is.EqualTo (1), "Sorting by modseq failed."); + Assert.That (messages[1].Index, Is.EqualTo (0), "Sorting by modseq failed."); + Assert.That (messages[2].Index, Is.EqualTo (2), "Sorting by modseq failed."); + + messages.Sort (new[] { new OrderByAnnotation (AnnotationEntry.AltSubject, AnnotationAttribute.SharedValue, SortOrder.Ascending) }); + Assert.That (messages[0].Index, Is.EqualTo (0), "Sorting by altsubject failed."); + Assert.That (messages[1].Index, Is.EqualTo (1), "Sorting by altsubject failed."); + Assert.That (messages[2].Index, Is.EqualTo (2), "Sorting by altsubject failed."); + + messages.Sort (new[] { new OrderByAnnotation (AnnotationEntry.AltSubject, AnnotationAttribute.SharedValue, SortOrder.Descending) }); + Assert.That (messages[0].Index, Is.EqualTo (2), "Sorting by reverse altsubject failed."); + Assert.That (messages[1].Index, Is.EqualTo (1), "Sorting by reverse altsubject failed."); + Assert.That (messages[2].Index, Is.EqualTo (0), "Sorting by reverse altsubject failed."); + } + + [Test] + public void TestSortingEnumerable () + { + var messages = Create (); + IEnumerable enumerable = messages; + IList sorted; + + sorted = enumerable.Sort (new [] { OrderBy.Arrival }); + Assert.That (sorted[0].Index, Is.EqualTo (0), "Sorting by arrival failed."); + Assert.That (sorted[1].Index, Is.EqualTo (1), "Sorting by arrival failed."); + Assert.That (sorted[2].Index, Is.EqualTo (2), "Sorting by arrival failed."); + + sorted = enumerable.Sort (new [] { OrderBy.ReverseArrival }); + Assert.That (sorted[0].Index, Is.EqualTo (2), "Sorting by reverse arrival failed."); + Assert.That (sorted[1].Index, Is.EqualTo (1), "Sorting by reverse arrival failed."); + Assert.That (sorted[2].Index, Is.EqualTo (0), "Sorting by reverse arrival failed."); + + sorted = enumerable.Sort (new [] { OrderBy.Subject }); + Assert.That (sorted[0].Index, Is.EqualTo (0), "Sorting by subject failed."); + Assert.That (sorted[1].Index, Is.EqualTo (1), "Sorting by subject failed."); + Assert.That (sorted[2].Index, Is.EqualTo (2), "Sorting by subject failed."); + + sorted = enumerable.Sort (new [] { OrderBy.ReverseSubject }); + Assert.That (sorted[0].Index, Is.EqualTo (2), "Sorting by reverse subject failed."); + Assert.That (sorted[1].Index, Is.EqualTo (1), "Sorting by reverse subject failed."); + Assert.That (sorted[2].Index, Is.EqualTo (0), "Sorting by reverse subject failed."); + + sorted = enumerable.Sort (new [] { OrderBy.Size, OrderBy.Arrival }); + Assert.That (sorted[0].Index, Is.EqualTo (1), "Sorting by size failed."); + Assert.That (sorted[1].Index, Is.EqualTo (0), "Sorting by size failed."); + Assert.That (sorted[2].Index, Is.EqualTo (2), "Sorting by size failed."); + + sorted = enumerable.Sort (new [] { OrderBy.Date }); + Assert.That (sorted[0].Index, Is.EqualTo (0), "Sorting by date failed."); + Assert.That (sorted[1].Index, Is.EqualTo (1), "Sorting by date failed."); + Assert.That (sorted[2].Index, Is.EqualTo (2), "Sorting by date failed."); + + sorted = enumerable.Sort (new [] { OrderBy.Size, OrderBy.Subject }); + Assert.That (sorted[0].Index, Is.EqualTo (1), "Sorting by size+subject failed."); + Assert.That (sorted[1].Index, Is.EqualTo (0), "Sorting by size+subject failed."); + Assert.That (sorted[2].Index, Is.EqualTo (2), "Sorting by size+subject failed."); + + sorted = enumerable.Sort (new [] { OrderBy.ReverseSize, OrderBy.ReverseSubject }); + Assert.That (sorted[0].Index, Is.EqualTo (2), "Sorting by reversed size+subject failed."); + Assert.That (sorted[1].Index, Is.EqualTo (0), "Sorting by reversed size+subject failed."); + Assert.That (sorted[2].Index, Is.EqualTo (1), "Sorting by reversed size+subject failed."); + + sorted = enumerable.Sort (new [] { OrderBy.DisplayFrom }); + Assert.That (sorted[0].Index, Is.EqualTo (0), "Sorting by display-from failed."); + Assert.That (sorted[1].Index, Is.EqualTo (1), "Sorting by display-from failed."); + Assert.That (sorted[2].Index, Is.EqualTo (2), "Sorting by display-from failed."); + + sorted = enumerable.Sort (new [] { OrderBy.From }); + Assert.That (sorted[0].Index, Is.EqualTo (0), "Sorting by from failed."); + Assert.That (sorted[1].Index, Is.EqualTo (1), "Sorting by from failed."); + Assert.That (sorted[2].Index, Is.EqualTo (2), "Sorting by from failed."); + + sorted = enumerable.Sort (new [] { OrderBy.DisplayTo }); + Assert.That (sorted[0].Index, Is.EqualTo (0), "Sorting by display-to failed."); + Assert.That (sorted[1].Index, Is.EqualTo (1), "Sorting by display-to failed."); + Assert.That (sorted[2].Index, Is.EqualTo (2), "Sorting by display-to failed."); + + sorted = enumerable.Sort (new [] { OrderBy.To }); + Assert.That (sorted[0].Index, Is.EqualTo (0), "Sorting by to failed."); + Assert.That (sorted[1].Index, Is.EqualTo (1), "Sorting by to failed."); + Assert.That (sorted[2].Index, Is.EqualTo (2), "Sorting by to failed."); + + sorted = enumerable.Sort (new [] { OrderBy.Cc }); + Assert.That (sorted[0].Index, Is.EqualTo (0), "Sorting by cc failed."); + Assert.That (sorted[1].Index, Is.EqualTo (1), "Sorting by cc failed."); + Assert.That (sorted[2].Index, Is.EqualTo (2), "Sorting by cc failed."); + + sorted = enumerable.Sort (new [] { new OrderBy (OrderByType.ModSeq, SortOrder.Ascending), OrderBy.Arrival }); + Assert.That (sorted[0].Index, Is.EqualTo (1), "Sorting by modseq failed."); + Assert.That (sorted[1].Index, Is.EqualTo (0), "Sorting by modseq failed."); + Assert.That (sorted[2].Index, Is.EqualTo (2), "Sorting by modseq failed."); + + sorted = enumerable.Sort (new[] { new OrderByAnnotation (AnnotationEntry.AltSubject, AnnotationAttribute.SharedValue, SortOrder.Ascending) }); + Assert.That (sorted[0].Index, Is.EqualTo (0), "Sorting by subject failed."); + Assert.That (sorted[1].Index, Is.EqualTo (1), "Sorting by subject failed."); + Assert.That (sorted[2].Index, Is.EqualTo (2), "Sorting by subject failed."); + + sorted = enumerable.Sort (new[] { new OrderByAnnotation (AnnotationEntry.AltSubject, AnnotationAttribute.SharedValue, SortOrder.Descending) }); + Assert.That (sorted[0].Index, Is.EqualTo (2), "Sorting by reverse subject failed."); + Assert.That (sorted[1].Index, Is.EqualTo (1), "Sorting by reverse subject failed."); + Assert.That (sorted[2].Index, Is.EqualTo (0), "Sorting by reverse subject failed."); } } } diff --git a/UnitTests/MessageSummaryTests.cs b/UnitTests/MessageSummaryTests.cs new file mode 100644 index 0000000000..1c8a9eff18 --- /dev/null +++ b/UnitTests/MessageSummaryTests.cs @@ -0,0 +1,504 @@ +// +// MessageSummaryTests.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using MimeKit; +using MailKit; + +namespace UnitTests { + [TestFixture] + public class MessageSummaryTests + { + [Test] + public void TestArgumentExceptions () + { + Assert.Throws (() => new MessageSummary (-1)); + Assert.Throws (() => new MessageSummary (null, 0)); + } + + [Test] + public void TestDefaultValues () + { + var summary = new MessageSummary (17); + + Assert.That (summary.Attachments.Count (), Is.EqualTo (0), "Attachments"); + Assert.That (summary.Body, Is.Null, "Body"); + Assert.That (summary.BodyParts.Count (), Is.EqualTo (0), "BodyParts"); + Assert.That (summary.Date, Is.EqualTo (DateTimeOffset.MinValue), "Date"); + Assert.That (summary.Envelope, Is.Null, "Envelope"); + Assert.That (summary.Flags, Is.Null, "Flags"); + Assert.That (summary.GMailLabels, Is.Null, "GMailLabels"); + Assert.That (summary.GMailMessageId, Is.Null, "GMailMessageId"); + Assert.That (summary.GMailThreadId, Is.Null, "GMailThreadId"); + Assert.That (summary.Headers, Is.Null, "Headers"); + Assert.That (summary.HtmlBody, Is.Null, "HtmlBody"); + Assert.That (summary.Index, Is.EqualTo (17), "Index"); + Assert.That (summary.InternalDate, Is.Null, "InternalDate"); + Assert.That (summary.IsReply, Is.False, "IsReply"); + Assert.That (summary.ModSeq, Is.Null, "ModSeq"); + Assert.That (summary.NormalizedSubject, Is.EqualTo (string.Empty), "NormalizedSubject"); + Assert.That (summary.PreviewText, Is.Null, "PreviewText"); + Assert.That (summary.References, Is.Null, "References"); + Assert.That (summary.Size, Is.Null, "Size"); + Assert.That (summary.TextBody, Is.Null, "TextBody"); + Assert.That (summary.UniqueId, Is.EqualTo (UniqueId.Invalid), "UniqueId"); + Assert.That (summary.Keywords, Is.Not.Null, "Keywords"); + Assert.That (summary.Keywords, Is.Empty, "Keywords"); + } + + [Test] + public void TestGMailProperties () + { + ulong msgid = 179111; + ulong thrid = 7192564; + var summary = new MessageSummary (0) { + GMailLabels = new List (), + GMailMessageId = msgid, + GMailThreadId = thrid + }; + + Assert.That (summary.GMailLabels, Is.Empty, "GMailLabels"); + Assert.That (summary.GMailMessageId, Is.EqualTo (msgid), "GMailMessageId"); + Assert.That (summary.GMailThreadId, Is.EqualTo (thrid), "GMailThreadId"); + } + + static ContentType CreateContentType (string type, string subtype, string partSpecifier) + { + var contentType = new ContentType (type, subtype); + contentType.Parameters.Add ("part-specifier", partSpecifier); + return contentType; + } + + static BodyPartMessage CreateMessage (string type, string subtype, string partSpecifier, BodyPart body, bool attachment) + { + var message = new BodyPartMessage (CreateContentType (type, subtype, partSpecifier), partSpecifier); + if (attachment) + message.ContentDisposition = new ContentDisposition (ContentDisposition.Attachment); + message.Body = body; + return message; + } + + static BodyPartMultipart CreateMultipart (string type, string subtype, string partSpecifier, params BodyPart [] bodyParts) + { + var multipart = new BodyPartMultipart (CreateContentType (type, subtype, partSpecifier), partSpecifier); + foreach (var bodyPart in bodyParts) + multipart.BodyParts.Add (bodyPart); + return multipart; + } + + static BodyPartBasic CreateBasic (string type, string subtype, string partSpecifier, bool attachment) + { + var basic = new BodyPartBasic (CreateContentType (type, subtype, partSpecifier), partSpecifier); + basic.ContentDisposition = new ContentDisposition (attachment ? ContentDisposition.Attachment : ContentDisposition.Inline); + return basic; + } + + static BodyPartText CreateText (string type, string subtype, string partSpecifier, bool attachment) + { + var text = new BodyPartText (CreateContentType (type, subtype, partSpecifier), partSpecifier); + if (attachment) + text.ContentDisposition = new ContentDisposition (ContentDisposition.Attachment); + return text; + } + + [Test] + public void TestTextPlainBody () + { + var summary = new MessageSummary (0) { + Body = CreateText ("TEXT", "PLAIN", "1", false) + }; + + var plain = summary.TextBody; + Assert.That (plain, Is.Not.Null, "TextBody"); + Assert.That (plain.ContentType.Parameters["part-specifier"], Is.EqualTo ("1"), "TextBody"); + + var html = summary.HtmlBody; + Assert.That (html, Is.Null, "HtmlBody"); + + Assert.That (summary.Attachments.Count (), Is.EqualTo (0), "Attachments"); + Assert.That (summary.BodyParts.Count (), Is.EqualTo (1), "BodyParts"); + } + + [Test] + public void TestTextHtmlBody () + { + var summary = new MessageSummary (0) { + Body = CreateText ("TEXT", "HTML", "1", false) + }; + + var html = summary.HtmlBody; + Assert.That (html, Is.Not.Null, "HtmlBody"); + Assert.That (html.ContentType.Parameters["part-specifier"], Is.EqualTo ("1"), "HtmlBody"); + + var plain = summary.TextBody; + Assert.That (plain, Is.Null, "TextBody"); + + Assert.That (summary.Attachments.Count (), Is.EqualTo (0), "Attachments"); + Assert.That (summary.BodyParts.Count (), Is.EqualTo (1), "BodyParts"); + } + + [Test] + public void TestImageJpegBody () + { + var summary = new MessageSummary (0) { + Body = CreateBasic ("IMAGE", "JPEG", "1", false) + }; + + var plain = summary.TextBody; + Assert.That (plain, Is.Null, "TextBody"); + + var html = summary.HtmlBody; + Assert.That (html, Is.Null, "HtmlBody"); + + Assert.That (summary.Attachments.Count (), Is.EqualTo (0), "Attachments"); + Assert.That (summary.BodyParts.Count (), Is.EqualTo (1), "BodyParts"); + } + + [Test] + public void TestMultipartAlternative () + { + var summary = new MessageSummary (0) { + Body = CreateMultipart ("MULTIPART", "ALTERNATIVE", "", + CreateText ("TEXT", "PLAIN", "1", false), + CreateText ("TEXT", "HTML", "2", false) + ) + }; + + var plain = summary.TextBody; + Assert.That (plain, Is.Not.Null, "TextBody"); + Assert.That (plain.ContentType.Parameters["part-specifier"], Is.EqualTo ("1"), "TextBody"); + + var html = summary.HtmlBody; + Assert.That (html, Is.Not.Null, "HtmlBody"); + Assert.That (html.ContentType.Parameters["part-specifier"], Is.EqualTo ("2"), "HtmlBody"); + + Assert.That (summary.Attachments.Count (), Is.EqualTo (0), "Attachments"); + Assert.That (summary.BodyParts.Count (), Is.EqualTo (2), "BodyParts"); + } + + [Test] + public void TestMultipartAlternativeNoTextParts () + { + var summary = new MessageSummary (0) { + Body = CreateMultipart ("MULTIPART", "ALTERNATIVE", "", + CreateText ("TEXT", "RICHTEXT", "1", false), + CreateBasic ("APPLICATION", "PDF", "2", false) + ) + }; + + var plain = summary.TextBody; + Assert.That (plain, Is.Null, "TextBody"); + + var html = summary.HtmlBody; + Assert.That (html, Is.Null, "HtmlBody"); + + Assert.That (summary.Attachments.Count (), Is.EqualTo (0), "Attachments"); + Assert.That (summary.BodyParts.Count (), Is.EqualTo (2), "BodyParts"); + } + + [Test] + public void TestMixedTextPlainBody () + { + var summary = new MessageSummary (0) { + Body = CreateMultipart ("MULTIPART", "MIXED", "", + CreateText ("TEXT", "PLAIN", "1", false), + CreateBasic ("IMAGE", "JPEG", "2", true) + ) + }; + + var plain = summary.TextBody; + Assert.That (plain, Is.Not.Null, "TextBody"); + Assert.That (plain.ContentType.Parameters["part-specifier"], Is.EqualTo ("1"), "TextBody"); + + var html = summary.HtmlBody; + Assert.That (html, Is.Null, "HtmlBody"); + + Assert.That (summary.Attachments.Count (), Is.EqualTo (1), "Attachments"); + Assert.That (summary.BodyParts.Count (), Is.EqualTo (2), "BodyParts"); + } + + [Test] + public void TestMixedTextHtmlBody () + { + var summary = new MessageSummary (0) { + Body = CreateMultipart ("MULTIPART", "MIXED", "", + CreateText ("TEXT", "HTML", "1", false), + CreateBasic ("IMAGE", "JPEG", "2", true) + ) + }; + + var plain = summary.TextBody; + Assert.That (plain, Is.Null, "TextBody"); + + var html = summary.HtmlBody; + Assert.That (html, Is.Not.Null, "HtmlBody"); + Assert.That (html.ContentType.Parameters["part-specifier"], Is.EqualTo ("1"), "HtmlBody"); + + Assert.That (summary.Attachments.Count (), Is.EqualTo (1), "Attachments"); + Assert.That (summary.BodyParts.Count (), Is.EqualTo (2), "BodyParts"); + } + + [Test] + public void TestRelatedTextPlainBody () + { + var summary = new MessageSummary (0) { + Body = CreateMultipart ("MULTIPART", "RELATED", "", + CreateText ("TEXT", "PLAIN", "1", false), + CreateBasic ("IMAGE", "JPEG", "2", true) + ) + }; + + var plain = summary.TextBody; + Assert.That (plain, Is.Not.Null, "TextBody"); + Assert.That (plain.ContentType.Parameters["part-specifier"], Is.EqualTo ("1"), "TextBody"); + + var html = summary.HtmlBody; + Assert.That (html, Is.Null, "HtmlBody"); + + Assert.That (summary.Attachments.Count (), Is.EqualTo (1), "Attachments"); + Assert.That (summary.BodyParts.Count (), Is.EqualTo (2), "BodyParts"); + } + + [Test] + public void TestRelatedTextHtmlBody () + { + var summary = new MessageSummary (0) { + Body = CreateMultipart ("MULTIPART", "RELATED", "", + CreateText ("TEXT", "HTML", "1", false), + CreateBasic ("IMAGE", "JPEG", "2", true) + ) + }; + + var plain = summary.TextBody; + Assert.That (plain, Is.Null, "TextBody"); + + var html = summary.HtmlBody; + Assert.That (html, Is.Not.Null, "HtmlBody"); + Assert.That (html.ContentType.Parameters ["part-specifier"], Is.EqualTo ("1"), "HtmlBody"); + + Assert.That (summary.Attachments.Count (), Is.EqualTo (1), "Attachments"); + Assert.That (summary.BodyParts.Count (), Is.EqualTo (2), "BodyParts"); + } + + [Test] + public void TestMixedAlternativeRelated () + { + var summary = new MessageSummary (0) { + Body = CreateMultipart ("MULTIPART", "MIXED", "", + CreateMultipart ("MULTIPART", "ALTERNATIVE", "1", + CreateText ("TEXT", "PLAIN", "1.1", false), + CreateMultipart ("MULTIPART", "RELATED", "1.2", + CreateText ("TEXT", "HTML", "1.2.1", false), + CreateBasic ("IMAGE", "JPEG", "1.2.2", false) + ) + ), + CreateBasic ("IMAGE", "JPEG", "2", true) + ) + }; + + var plain = summary.TextBody; + Assert.That (plain, Is.Not.Null, "TextBody"); + Assert.That (plain.ContentType.Parameters["part-specifier"], Is.EqualTo ("1.1"), "TextBody"); + + var html = summary.HtmlBody; + Assert.That (html, Is.Not.Null, "HtmlBody"); + Assert.That (html.ContentType.Parameters["part-specifier"], Is.EqualTo ("1.2.1"), "HtmlBody"); + + Assert.That (summary.Attachments.Count (), Is.EqualTo (1), "Attachments"); + Assert.That (summary.BodyParts.Count (), Is.EqualTo (4), "BodyParts"); + } + + [Test] + public void TestMixedAlternativeRelatedWithStartParameter () + { + var summary = new MessageSummary (0) { + Body = CreateMultipart ("MULTIPART", "MIXED", "", + CreateMultipart ("MULTIPART", "ALTERNATIVE", "1", + CreateText ("TEXT", "PLAIN", "1.1", false), + CreateMultipart ("MULTIPART", "RELATED", "1.2", + CreateBasic ("IMAGE", "JPEG", "1.2.1", false), + CreateText ("TEXT", "HTML", "1.2.2", false) + ) + ), + CreateBasic ("IMAGE", "JPEG", "2", true) + ) + }; + var cid = "html@localhost.com"; + var mixed = (BodyPartMultipart) summary.Body; + var alternative = (BodyPartMultipart) mixed.BodyParts[0]; + var related = (BodyPartMultipart) alternative.BodyParts[1]; + var html = (BodyPartText) related.BodyParts[1]; + + related.ContentType.Parameters["start"] = cid; + html.ContentLocation = new Uri ("cid:" + cid); + + var plain = summary.TextBody; + Assert.That (plain, Is.Not.Null, "TextBody"); + Assert.That (plain.ContentType.Parameters["part-specifier"], Is.EqualTo ("1.1"), "TextBody"); + + html = summary.HtmlBody; + Assert.That (html, Is.Not.Null, "HtmlBody"); + Assert.That (html.ContentType.Parameters["part-specifier"], Is.EqualTo ("1.2.2"), "HtmlBody"); + + Assert.That (summary.Attachments.Count (), Is.EqualTo (1), "Attachments"); + Assert.That (summary.BodyParts.Count (), Is.EqualTo (4), "BodyParts"); + } + + [Test] + public void TestMixedRelatedAlternativeWithStartParameter () + { + var summary = new MessageSummary (0) { + Body = CreateMultipart ("MULTIPART", "MIXED", "", + CreateMultipart ("MULTIPART", "RELATED", "1", + CreateBasic ("IMAGE", "JPEG", "1.1", false), + CreateMultipart ("MULTIPART", "ALTERNATIVE", "1.2", + CreateText ("TEXT", "PLAIN", "1.2.1", false), + CreateText ("TEXT", "HTML", "1.2.2", false) + ) + ), + CreateBasic ("IMAGE", "JPEG", "2", true) + ) + }; + var cid = "alternative@localhost.com"; + var mixed = (BodyPartMultipart) summary.Body; + var related = (BodyPartMultipart) mixed.BodyParts[0]; + var alternative = (BodyPartMultipart) related.BodyParts[1]; + + related.ContentType.Parameters["start"] = cid; + alternative.ContentLocation = new Uri ("cid:" + cid); + + var plain = summary.TextBody; + Assert.That (plain, Is.Not.Null, "TextBody"); + Assert.That (plain.ContentType.Parameters["part-specifier"], Is.EqualTo ("1.2.1"), "TextBody"); + + var html = summary.HtmlBody; + Assert.That (html, Is.Not.Null, "HtmlBody"); + Assert.That (html.ContentType.Parameters["part-specifier"], Is.EqualTo ("1.2.2"), "HtmlBody"); + + Assert.That (summary.Attachments.Count (), Is.EqualTo (1), "Attachments"); + Assert.That (summary.BodyParts.Count (), Is.EqualTo (4), "BodyParts"); + } + + [Test] + public void TestMixedRelatedAlternative () + { + var summary = new MessageSummary (0) { + Body = CreateMultipart ("MULTIPART", "MIXED", "", + CreateMultipart ("MULTIPART", "RELATED", "1", + CreateMultipart ("MULTIPART", "ALTERNATIVE", "1.1", + CreateText ("TEXT", "PLAIN", "1.1.1", false), + CreateText ("TEXT", "HTML", "1.1.2", false) + ), + CreateBasic ("IMAGE", "JPEG", "1.2", false) + ), + CreateBasic ("IMAGE", "JPEG", "2", true) + ) + }; + + var plain = summary.TextBody; + Assert.That (plain, Is.Not.Null, "TextBody"); + Assert.That (plain.ContentType.Parameters["part-specifier"], Is.EqualTo ("1.1.1"), "TextBody"); + + var html = summary.HtmlBody; + Assert.That (html, Is.Not.Null, "HtmlBody"); + Assert.That (html.ContentType.Parameters["part-specifier"], Is.EqualTo ("1.1.2"), "HtmlBody"); + + Assert.That (summary.Attachments.Count (), Is.EqualTo (1), "Attachments"); + Assert.That (summary.BodyParts.Count (), Is.EqualTo (4), "BodyParts"); + } + + [Test] + public void TestMixedNestedAlternative () + { + var summary = new MessageSummary (0) { + Body = CreateMultipart ("MULTIPART", "MIXED", "", + CreateMultipart ("MULTIPART", "ALTERNATIVE", "1", + CreateMultipart ("MULTIPART", "ALTERNATIVE", "1.1", + CreateText ("TEXT", "PLAIN", "1.1.1", false), + CreateText ("TEXT", "HTML", "1.1.2", false) + ) + ), + CreateBasic ("IMAGE", "JPEG", "2", true) + ) + }; + + var plain = summary.TextBody; + Assert.That (plain, Is.Not.Null, "TextBody"); + Assert.That (plain.ContentType.Parameters["part-specifier"], Is.EqualTo ("1.1.1"), "TextBody"); + + var html = summary.HtmlBody; + Assert.That (html, Is.Not.Null, "HtmlBody"); + Assert.That (html.ContentType.Parameters["part-specifier"], Is.EqualTo ("1.1.2"), "HtmlBody"); + + Assert.That (summary.Attachments.Count (), Is.EqualTo (1), "Attachments"); + Assert.That (summary.BodyParts.Count (), Is.EqualTo (3), "BodyParts"); + } + + [Test] + public void TestComplexBody () + { + var summary = new MessageSummary (0) { + Body = CreateMultipart ("MULTIPART", "MIXED", "", + CreateMultipart ("MULTIPART", "ALTERNATIVE", "1", + CreateText ("TEXT", "PLAIN", "1.1", false), + CreateMultipart ("MULTIPART", "RELATED", "1.2", + CreateText ("TEXT", "HTML", "1.2.1", false), + CreateBasic ("IMAGE", "JPEG", "1.2.2", false) + ) + ), + CreateBasic ("APPLICATION", "OCTET-STREAM", "2", true), + CreateMessage ("MESSAGE", "RFC822", "3", + CreateMultipart ("MULTIPART", "MIXED", "3", + CreateText ("TEXT", "PLAIN", "3.1", false), + CreateBasic ("APPLICATION", "OCTET-STREAM", "3.2", true) + ), true + ), + CreateBasic ("IMAGE", "GIF", "4", true) + ) + }; + int i; + + var plain = summary.TextBody; + Assert.That (plain, Is.Not.Null, "TextBody"); + Assert.That (plain.ContentType.Parameters["part-specifier"], Is.EqualTo ("1.1"), "TextBody"); + + var html = summary.HtmlBody; + Assert.That (html, Is.Not.Null, "HtmlBody"); + Assert.That (html.ContentType.Parameters["part-specifier"], Is.EqualTo ("1.2.1"), "HtmlBody"); + + var bodyParts = new string [] { "1.1", "1.2.1", "1.2.2", "2", "3", "4" }; + i = 0; + + foreach (var part in summary.BodyParts) + Assert.That (part.ContentType.Parameters["part-specifier"], Is.EqualTo (bodyParts[i++]), "BodyParts"); + + var attachments = new string[] { "2", "3", "4" }; + i = 0; + + foreach (var attachment in summary.Attachments) + Assert.That (attachment.ContentType.Parameters["part-specifier"], Is.EqualTo (attachments[i++]), "Attachments"); + } + } +} diff --git a/UnitTests/MessageThreadingTests.cs b/UnitTests/MessageThreadingTests.cs index 7d2615201f..f26d165df6 100644 --- a/UnitTests/MessageThreadingTests.cs +++ b/UnitTests/MessageThreadingTests.cs @@ -1,9 +1,9 @@ -// +// // MessageThreadingTests.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -24,12 +24,7 @@ // THE SOFTWARE. // -using System; -using System.Linq; using System.Text; -using System.Collections.Generic; - -using NUnit.Framework; using MimeKit; using MimeKit.Utils; @@ -46,8 +41,7 @@ public void TestArgumentExceptions () { var orderBy = new OrderBy[] { OrderBy.Arrival }; var messagesMissingInfo = new [] { new MessageSummary (0) }; - var emptyOrderBy = new OrderBy[0]; - int depth; + var emptyOrderBy = Array.Empty (); var summary = new MessageSummary (0); summary.UniqueId = UniqueId.MinValue; @@ -60,7 +54,7 @@ public void TestArgumentExceptions () var messages = new MessageSummary[] { summary }; - Assert.Throws (() => MessageThreader.GetThreadableSubject (null, out depth)); + Assert.Throws (() => MessageThreader.GetThreadableSubject (null, out _)); Assert.Throws (() => MessageThreader.Thread ((IEnumerable) null, ThreadingAlgorithm.References)); Assert.Throws (() => MessageThreader.Thread ((IEnumerable) null, ThreadingAlgorithm.References, orderBy)); Assert.Throws (() => MessageThreader.Thread (messagesMissingInfo, ThreadingAlgorithm.References)); @@ -75,38 +69,38 @@ public void TestThreadableSubject () int depth; result = MessageThreader.GetThreadableSubject ("Re: simple subject", out depth); - Assert.AreEqual ("simple subject", result, "#1a"); - Assert.AreEqual (1, depth, "#1b"); + Assert.That (result, Is.EqualTo ("simple subject"), "#1a"); + Assert.That (depth, Is.EqualTo (1), "#1b"); result = MessageThreader.GetThreadableSubject ("Re: simple subject ", out depth); - Assert.AreEqual ("simple subject", result, "#2a"); - Assert.AreEqual (1, depth, "#2b"); + Assert.That (result, Is.EqualTo ("simple subject"), "#2a"); + Assert.That (depth, Is.EqualTo (1), "#2b"); result = MessageThreader.GetThreadableSubject ("Re: Re: simple subject ", out depth); - Assert.AreEqual ("simple subject", result, "#3a"); - Assert.AreEqual (2, depth, "#3b"); + Assert.That (result, Is.EqualTo ("simple subject"), "#3a"); + Assert.That (depth, Is.EqualTo (2), "#3b"); result = MessageThreader.GetThreadableSubject ("Re: Re[4]: simple subject ", out depth); - Assert.AreEqual ("simple subject", result, "#4a"); - Assert.AreEqual (5, depth, "#4b"); + Assert.That (result, Is.EqualTo ("simple subject"), "#4a"); + Assert.That (depth, Is.EqualTo (5), "#4b"); result = MessageThreader.GetThreadableSubject ("Re: [Mailing-List] Re[4]: simple subject ", out depth); - Assert.AreEqual ("simple subject", result, "#5a"); - Assert.AreEqual (5, depth, "#5b"); + Assert.That (result, Is.EqualTo ("simple subject"), "#5a"); + Assert.That (depth, Is.EqualTo (5), "#5b"); } - MessageSummary MakeThreadable (ref int index, string subject, string msgid, string date, string refs) - { - DateTimeOffset value; + static readonly char[] Space = new[] { ' ' }; - DateUtils.TryParse (date, out value); + static MessageSummary MakeThreadable (ref int index, string subject, string msgid, string date, string refs) + { + DateUtils.TryParse (date, out var value); var summary = new MessageSummary (++index); summary.UniqueId = new UniqueId ((uint) summary.Index); summary.Envelope = new Envelope (); summary.References = new MessageIdList (); if (refs != null) { - foreach (var id in refs.Split (new [] { ' ' }, StringSplitOptions.RemoveEmptyEntries)) + foreach (var id in refs.Split (Space, StringSplitOptions.RemoveEmptyEntries)) summary.References.Add (id); } summary.Envelope.MessageId = MimeUtils.EnumerateReferences (msgid).FirstOrDefault (); @@ -117,7 +111,7 @@ MessageSummary MakeThreadable (ref int index, string subject, string msgid, stri return summary; } - void WriteMessageThread (StringBuilder builder, IList messages, MessageThread thread, int depth) + static void WriteMessageThread (StringBuilder builder, IList messages, MessageThread thread, int depth) { builder.Append (new string (' ', depth * 3)); @@ -163,7 +157,7 @@ public void TestThreadBySubject () //Console.WriteLine (builder); - Assert.AreEqual (expected, builder.ToString (), "Threading did not produce the expected results"); + Assert.That (builder.ToString (), Is.EqualTo (expected), "Threading did not produce the expected results"); } [Test] @@ -306,7 +300,31 @@ Welcome to Netscape //Console.WriteLine (builder); - Assert.AreEqual (expected, builder.ToString (), "Threading did not produce the expected results"); + Assert.That (builder.ToString (), Is.EqualTo (expected), "Threading did not produce the expected results"); + } + + [Test] + public void TestThreadableNodeUnusedProperties () + { + var node = new MessageThreader.ThreadableNode (new MessageSummary (0)); + + Assert.That (node.Folder, Is.Null, "Folder"); + Assert.That (node.Body, Is.Null, "Body"); + Assert.That (node.TextBody, Is.Null, "TextBody"); + Assert.That (node.HtmlBody, Is.Null, "HtmlBody"); + Assert.That (node.BodyParts, Is.Empty, "BodyParts"); + Assert.That (node.Attachments, Is.Empty, "Attachments"); + Assert.That (node.PreviewText, Is.Null, "PreviewText"); + Assert.That (node.Envelope, Is.Null, "Envelope"); + Assert.That (node.Flags.HasValue, Is.False, "Flags"); + Assert.That (node.Keywords, Is.Empty, "Keywords"); + Assert.That (node.Headers, Is.Null, "Headers"); + Assert.That (node.InternalDate.HasValue, Is.False, "InternalDate"); + Assert.That (node.EmailId, Is.Null, "EmailId"); + Assert.That (node.ThreadId, Is.Null, "ThreadId"); + Assert.That (node.GMailMessageId.HasValue, Is.False, "GMailMessageId"); + Assert.That (node.GMailThreadId.HasValue, Is.False, "GMailThreadId"); + Assert.That (node.GMailLabels, Is.Null, "GMailLabels"); } } } diff --git a/UnitTests/MetadataTests.cs b/UnitTests/MetadataTests.cs new file mode 100644 index 0000000000..da0f2be96b --- /dev/null +++ b/UnitTests/MetadataTests.cs @@ -0,0 +1,73 @@ +// +// MetadataTests.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using MailKit; + +namespace UnitTests { + [TestFixture] + public class MetadataTests + { + [Test] + public void TestMetadataTag () + { + Assert.Throws (() => new MetadataTag (null)); + Assert.Throws (() => new MetadataTag (string.Empty)); + + var tag1 = new MetadataTag ("/dev/null"); + var tag2 = new MetadataTag ("/dev/null"); + var tag3 = new MetadataTag ("/opt/nope"); + + Assert.That (tag1.Equals (tag2), Is.True, "Equals #1"); + Assert.That (tag1.Equals (tag3), Is.False, "Equals #2"); + Assert.That (tag2.GetHashCode (), Is.EqualTo (tag1.GetHashCode ()), "GetHashCode #1"); + Assert.That (tag3.GetHashCode (), Is.Not.EqualTo (tag1.GetHashCode ()), "GetHashCode #2"); + + Assert.That (MetadataTag.Create (MetadataTag.PrivateComment.ToString ()), Is.EqualTo (MetadataTag.PrivateComment)); + Assert.That (MetadataTag.Create (MetadataTag.PrivateSpecialUse.ToString ()), Is.EqualTo (MetadataTag.PrivateSpecialUse)); + Assert.That (MetadataTag.Create (MetadataTag.SharedAdmin.ToString ()), Is.EqualTo (MetadataTag.SharedAdmin)); + Assert.That (MetadataTag.Create (MetadataTag.SharedComment.ToString ()), Is.EqualTo (MetadataTag.SharedComment)); + Assert.That (MetadataTag.Create (tag1.Id), Is.EqualTo (tag1)); + } + + [Test] + public void TestMetadataOptions () + { + var options = new MetadataOptions (); + + Assert.That (options.Depth, Is.EqualTo (0)); + Assert.That (options.LongEntries, Is.EqualTo (0)); + Assert.That (options.MaxSize, Is.Null); + + Assert.Throws (() => options.Depth = 500); + } + + [Test] + public void TestMetadataCollection () + { + Assert.Throws (() => new MetadataCollection (null)); + } + } +} diff --git a/UnitTests/Net/DummyNetworkStream.cs b/UnitTests/Net/DummyNetworkStream.cs new file mode 100644 index 0000000000..e120581b1f --- /dev/null +++ b/UnitTests/Net/DummyNetworkStream.cs @@ -0,0 +1,60 @@ +// +// DummyNetworkStream.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + + +namespace UnitTests.Net { + public class DummyNetworkStream : MemoryStream + { + readonly bool throwOnWrite; + + public DummyNetworkStream (bool throwOnWrite = false) + { + this.throwOnWrite = throwOnWrite; + } + + public override bool CanSeek => false; + public override bool CanTimeout => true; + + public override int ReadTimeout { get; set; } + public override int WriteTimeout { get; set; } + + public override void Write (byte[] buffer, int offset, int count) + { + if (throwOnWrite) + throw new IOException (); + + base.Write (buffer, offset, count); + } + + public override Task WriteAsync (byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if (throwOnWrite) + throw new IOException (); + + return base.WriteAsync (buffer, offset, count, cancellationToken); + } + } +} diff --git a/UnitTests/Net/Imap/ImapAuthenticationSecretDetectorTests.cs b/UnitTests/Net/Imap/ImapAuthenticationSecretDetectorTests.cs new file mode 100644 index 0000000000..77879f8176 --- /dev/null +++ b/UnitTests/Net/Imap/ImapAuthenticationSecretDetectorTests.cs @@ -0,0 +1,396 @@ +// +// ImapAuthenticationSecretDetectorTests.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System.Text; + +using MailKit; +using MailKit.Net.Imap; + +namespace UnitTests.Net.Imap { + [TestFixture] + public class ImapAuthenticationSecretDetectorTests + { + [Test] + public void TestEmptyCommand () + { + var detector = new ImapAuthenticationSecretDetector (); + var buffer = Array.Empty (); + + detector.IsAuthenticating = true; + + var secrets = detector.DetectSecrets (buffer, 0, buffer.Length); + Assert.That (secrets, Is.Empty, "# of secrets"); + } + + [Test] + public void TestNonAuthCommand () + { + string command = string.Format ("A00000000 APPEND INBOX (\\Seen) \"{0}\" {{4096}}\r\n", ImapUtils.FormatInternalDate (DateTimeOffset.Now)); + var detector = new ImapAuthenticationSecretDetector (); + var buffer = Encoding.ASCII.GetBytes (command); + + detector.IsAuthenticating = true; + + var secrets = detector.DetectSecrets (buffer, 0, buffer.Length); + Assert.That (secrets, Is.Empty, "# of secrets"); + } + + [Test] + public void TestNotIsAuthenticating () + { + const string command = "A00000000 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n"; + var detector = new ImapAuthenticationSecretDetector (); + var buffer = Encoding.ASCII.GetBytes (command); + + var secrets = detector.DetectSecrets (buffer, 0, buffer.Length); + Assert.That (secrets, Is.Empty, "# of secrets"); + } + + [Test] + public void TestLoginCommand () + { + const string command = "A00000000 LOGIN username password\r\n"; + var userIndex = command.IndexOf ("username", StringComparison.Ordinal); + var passwdIndex = command.IndexOf ("password", StringComparison.Ordinal); + var detector = new ImapAuthenticationSecretDetector (); + var buffer = Encoding.ASCII.GetBytes (command); + + detector.IsAuthenticating = true; + + var secrets = detector.DetectSecrets (buffer, 0, buffer.Length); + Assert.That (secrets, Has.Count.EqualTo (2), "# of secrets"); + Assert.That (secrets[0].StartIndex, Is.EqualTo (userIndex), "UserName StartIndex"); + Assert.That (secrets[0].Length, Is.EqualTo (8), "UserName Length"); + Assert.That (secrets[1].StartIndex, Is.EqualTo (passwdIndex), "Password StartIndex"); + Assert.That (secrets[1].Length, Is.EqualTo (8), "Password Length"); + } + + [Test] + public void TestLoginCommandBitByBit () + { + const string command = "A00000000 LOGIN username password\r\n"; + var userIndex = command.IndexOf ("username", StringComparison.Ordinal); + var passwdIndex = command.IndexOf ("password", StringComparison.Ordinal); + var detector = new ImapAuthenticationSecretDetector (); + var buffer = Encoding.ASCII.GetBytes (command); + IList secrets; + int index = 0; + + detector.IsAuthenticating = true; + + while (index < command.Length) { + secrets = detector.DetectSecrets (buffer, index, 1); + if ((index >= userIndex && index < userIndex + 8) || (index >= passwdIndex && index < passwdIndex + 8)) { + Assert.That (secrets, Has.Count.EqualTo (1), $"# of secrets @ index {index}"); + Assert.That (secrets[0].StartIndex, Is.EqualTo (index), "StartIndex"); + Assert.That (secrets[0].Length, Is.EqualTo (1), "Length"); + } else { + Assert.That (secrets, Is.Empty, $"# of secrets @ index {index}"); + } + index++; + } + } + + [Test] + public void TestLoginCommandQStrings () + { + const string command = "A00000000 LOGIN \"username\" \"password\"\r\n"; + var userIndex = command.IndexOf ("username", StringComparison.Ordinal); + var passwdIndex = command.IndexOf ("password", StringComparison.Ordinal); + var detector = new ImapAuthenticationSecretDetector (); + var buffer = Encoding.ASCII.GetBytes (command); + + detector.IsAuthenticating = true; + + var secrets = detector.DetectSecrets (buffer, 0, buffer.Length); + Assert.That (secrets, Has.Count.EqualTo (2), "# of secrets"); + Assert.That (secrets[0].StartIndex, Is.EqualTo (userIndex), "UserName StartIndex"); + Assert.That (secrets[0].Length, Is.EqualTo (8), "UserName Length"); + Assert.That (secrets[1].StartIndex, Is.EqualTo (passwdIndex), "Password StartIndex"); + Assert.That (secrets[1].Length, Is.EqualTo (8), "Password Length"); + } + + [Test] + public void TestLoginCommandQStringsBitByBit () + { + const string command = "A00000000 LOGIN \"username\" \"password\"\r\n"; + var userIndex = command.IndexOf ("username", StringComparison.Ordinal); + var passwdIndex = command.IndexOf ("password", StringComparison.Ordinal); + var detector = new ImapAuthenticationSecretDetector (); + var buffer = Encoding.ASCII.GetBytes (command); + IList secrets; + int index = 0; + + detector.IsAuthenticating = true; + + while (index < command.Length) { + secrets = detector.DetectSecrets (buffer, index, 1); + if ((index >= userIndex && index < userIndex + 8) || (index >= passwdIndex && index < passwdIndex + 8)) { + Assert.That (secrets, Has.Count.EqualTo (1), $"# of secrets @ index {index}"); + Assert.That (secrets[0].StartIndex, Is.EqualTo (index), "StartIndex"); + Assert.That (secrets[0].Length, Is.EqualTo (1), "Length"); + } else { + Assert.That (secrets, Is.Empty, $"# of secrets @ index {index}"); + } + index++; + } + } + + [Test] + public void TestLoginCommandEscapedQStrings () + { + const string command = "A00000000 LOGIN \"domain\\\\username\" \"pass\\\"word\"\r\n"; + var userIndex = command.IndexOf ("domain\\\\username", StringComparison.Ordinal); + var passwdIndex = command.IndexOf ("pass\\\"word", StringComparison.Ordinal); + var detector = new ImapAuthenticationSecretDetector (); + var buffer = Encoding.ASCII.GetBytes (command); + + detector.IsAuthenticating = true; + + var secrets = detector.DetectSecrets (buffer, 0, buffer.Length); + Assert.That (secrets, Has.Count.EqualTo (2), "# of secrets"); + Assert.That (secrets[0].StartIndex, Is.EqualTo (userIndex), "UserName StartIndex"); + Assert.That (secrets[0].Length, Is.EqualTo (16), "UserName Length"); + Assert.That (secrets[1].StartIndex, Is.EqualTo (passwdIndex), "Password StartIndex"); + Assert.That (secrets[1].Length, Is.EqualTo (10), "Password Length"); + } + + [Test] + public void TestLoginCommandEscapedQStringsBitByBit () + { + const string command = "A00000000 LOGIN \"domain\\\\username\" \"pass\\\"word\"\r\n"; + var userIndex = command.IndexOf ("domain\\\\username", StringComparison.Ordinal); + var passwdIndex = command.IndexOf ("pass\\\"word", StringComparison.Ordinal); + var detector = new ImapAuthenticationSecretDetector (); + var buffer = Encoding.ASCII.GetBytes (command); + IList secrets; + int index = 0; + + detector.IsAuthenticating = true; + + while (index < command.Length) { + secrets = detector.DetectSecrets (buffer, index, 1); + if ((index >= userIndex && index < userIndex + 16) || (index >= passwdIndex && index < passwdIndex + 10)) { + Assert.That (secrets, Has.Count.EqualTo (1), $"# of secrets @ index {index}"); + Assert.That (secrets[0].StartIndex, Is.EqualTo (index), "StartIndex"); + Assert.That (secrets[0].Length, Is.EqualTo (1), "Length"); + } else { + Assert.That (secrets, Is.Empty, $"# of secrets @ index {index}"); + } + index++; + } + } + + [Test] + public void TestLoginCommandLiterals () + { + var detector = new ImapAuthenticationSecretDetector (); + IList secrets; + byte[] buffer; + + detector.IsAuthenticating = true; + + buffer = Encoding.ASCII.GetBytes ("A00000000 LOGIN {8}\r\n"); + secrets = detector.DetectSecrets (buffer, 0, buffer.Length); + Assert.That (secrets, Is.Empty, "LOGIN # of secrets"); + + buffer = Encoding.ASCII.GetBytes ("username {8}\r\n"); + secrets = detector.DetectSecrets (buffer, 0, buffer.Length); + Assert.That (secrets, Has.Count.EqualTo (1), "username # of secrets"); + Assert.That (secrets[0].StartIndex, Is.EqualTo (0), "UserName StartIndex"); + Assert.That (secrets[0].Length, Is.EqualTo (8), "UserName Length"); + + buffer = Encoding.ASCII.GetBytes ("password\r\n"); + secrets = detector.DetectSecrets (buffer, 0, buffer.Length); + Assert.That (secrets, Has.Count.EqualTo (1), "password # of secrets"); + Assert.That (secrets[0].StartIndex, Is.EqualTo (0), "Password StartIndex"); + Assert.That (secrets[0].Length, Is.EqualTo (8), "Password Length"); + } + + [Test] + public void TestLoginCommandLiteralsBitByBit () + { + const string command = "A00000000 LOGIN {8}\r\nusername {8}\r\npassword\r\n"; + var userIndex = command.IndexOf ("username", StringComparison.Ordinal); + var passwdIndex = command.IndexOf ("password", StringComparison.Ordinal); + var detector = new ImapAuthenticationSecretDetector (); + var buffer = Encoding.ASCII.GetBytes (command); + IList secrets; + int index = 0; + + detector.IsAuthenticating = true; + + while (index < command.Length) { + secrets = detector.DetectSecrets (buffer, index, 1); + if ((index >= userIndex && index < userIndex + 8) || (index >= passwdIndex && index < passwdIndex + 8)) { + Assert.That (secrets, Has.Count.EqualTo (1), $"# of secrets @ index {index}"); + Assert.That (secrets[0].StartIndex, Is.EqualTo (index), "StartIndex"); + Assert.That (secrets[0].Length, Is.EqualTo (1), "Length"); + } else { + Assert.That (secrets, Is.Empty, $"# of secrets @ index {index}"); + } + index++; + } + } + + [Test] + public void TestLoginCommandLiteralPlus () + { + const string command = "A00000000 LOGIN {8+}\r\nusername {8+}\r\npassword\r\n"; + var userIndex = command.IndexOf ("username", StringComparison.Ordinal); + var passwdIndex = command.IndexOf ("password", StringComparison.Ordinal); + var detector = new ImapAuthenticationSecretDetector (); + var buffer = Encoding.ASCII.GetBytes (command); + + detector.IsAuthenticating = true; + + var secrets = detector.DetectSecrets (buffer, 0, buffer.Length); + Assert.That (secrets, Has.Count.EqualTo (2), "# of secrets"); + Assert.That (secrets[0].StartIndex, Is.EqualTo (userIndex), "UserName StartIndex"); + Assert.That (secrets[0].Length, Is.EqualTo (8), "UserName Length"); + Assert.That (secrets[1].StartIndex, Is.EqualTo (passwdIndex), "Password StartIndex"); + Assert.That (secrets[1].Length, Is.EqualTo (8), "Password Length"); + } + + [Test] + public void TestLoginCommandLiteralPlusBitByBit () + { + const string command = "A00000000 LOGIN {8+}\r\nusername {8+}\r\npassword\r\n"; + var userIndex = command.IndexOf ("username", StringComparison.Ordinal); + var passwdIndex = command.IndexOf ("password", StringComparison.Ordinal); + var detector = new ImapAuthenticationSecretDetector (); + var buffer = Encoding.ASCII.GetBytes (command); + IList secrets; + int index = 0; + + detector.IsAuthenticating = true; + + while (index < command.Length) { + secrets = detector.DetectSecrets (buffer, index, 1); + if ((index >= userIndex && index < userIndex + 8) || (index >= passwdIndex && index < passwdIndex + 8)) { + Assert.That (secrets, Has.Count.EqualTo (1), $"# of secrets @ index {index}"); + Assert.That (secrets[0].StartIndex, Is.EqualTo (index), "StartIndex"); + Assert.That (secrets[0].Length, Is.EqualTo (1), "Length"); + } else { + Assert.That (secrets, Is.Empty, $"# of secrets @ index {index}"); + } + index++; + } + } + + [Test] + public void TestSaslIRAuthCommand () + { + const string command = "A00000000 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n"; + var secretIndex = command.IndexOf ("AHVzZXJuYW1lAHBhc3N3b3Jk", StringComparison.Ordinal); + var detector = new ImapAuthenticationSecretDetector (); + var buffer = Encoding.ASCII.GetBytes (command); + + detector.IsAuthenticating = true; + + var secrets = detector.DetectSecrets (buffer, 0, buffer.Length); + Assert.That (secrets, Has.Count.EqualTo (1), "# of secrets"); + Assert.That (secrets[0].StartIndex, Is.EqualTo (secretIndex), "StartIndex"); + Assert.That (secrets[0].Length, Is.EqualTo (24), "Length"); + } + + [Test] + public void TestSaslIRAuthCommandBitByBit () + { + const string command = "A00000000 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n"; + var secretIndex = command.IndexOf ("AHVzZXJuYW1lAHBhc3N3b3Jk", StringComparison.Ordinal); + var detector = new ImapAuthenticationSecretDetector (); + var buffer = Encoding.ASCII.GetBytes (command); + IList secrets; + int index = 0; + + detector.IsAuthenticating = true; + + while (index < command.Length) { + secrets = detector.DetectSecrets (buffer, index, 1); + if (index >= secretIndex && command[index] != '\r' && command[index] != '\n') { + Assert.That (secrets, Has.Count.EqualTo (1), $"# of secrets @ index {index}"); + Assert.That (secrets[0].StartIndex, Is.EqualTo (index), "StartIndex"); + Assert.That (secrets[0].Length, Is.EqualTo (1), "Length"); + } else { + Assert.That (secrets, Is.Empty, $"# of secrets @ index {index}"); + } + index++; + } + } + + [Test] + public void TestMultiLineSaslAuthCommand () + { + var detector = new ImapAuthenticationSecretDetector (); + IList secrets; + byte[] buffer; + + detector.IsAuthenticating = true; + + buffer = Encoding.ASCII.GetBytes ("A00000000 AUTHENTICATE LOGIN\r\n"); + secrets = detector.DetectSecrets (buffer, 0, buffer.Length); + Assert.That (secrets, Is.Empty, "initial # of secrets"); + + buffer = Encoding.ASCII.GetBytes ("dXNlcm5hbWU=\r\n"); + secrets = detector.DetectSecrets (buffer, 0, buffer.Length); + Assert.That (secrets, Has.Count.EqualTo (1), "# of secrets"); + Assert.That (secrets[0].StartIndex, Is.EqualTo (0), "StartIndex"); + Assert.That (secrets[0].Length, Is.EqualTo (12), "Length"); + + buffer = Encoding.ASCII.GetBytes ("cGFzc3dvcmQ=\r\n"); + secrets = detector.DetectSecrets (buffer, 0, buffer.Length); + Assert.That (secrets, Has.Count.EqualTo (1), "# of secrets"); + Assert.That (secrets[0].StartIndex, Is.EqualTo (0), "StartIndex"); + Assert.That (secrets[0].Length, Is.EqualTo (12), "Length"); + } + + [Test] + public void TestMultiLineSaslAuthCommandBitByBit () + { + const string command = "A00000000 AUTHENTICATE LOGIN\r\ndXNlcm5hbWU=\r\ncGFzc3dvcmQ=\r\n"; + var secretIndex = command.IndexOf ("dXNlcm5hbWU=", StringComparison.Ordinal); + var detector = new ImapAuthenticationSecretDetector (); + var buffer = Encoding.ASCII.GetBytes (command); + IList secrets; + int index = 0; + + detector.IsAuthenticating = true; + + while (index < command.Length) { + secrets = detector.DetectSecrets (buffer, index, 1); + if (index >= secretIndex && command[index] != '\r' && command[index] != '\n') { + Assert.That (secrets, Has.Count.EqualTo (1), $"# of secrets @ index {index}"); + Assert.That (secrets[0].StartIndex, Is.EqualTo (index), "StartIndex"); + Assert.That (secrets[0].Length, Is.EqualTo (1), "Length"); + } else { + Assert.That (secrets, Is.Empty, $"# of secrets @ index {index}"); + } + index++; + } + } + } +} diff --git a/UnitTests/Net/Imap/ImapClientTests.cs b/UnitTests/Net/Imap/ImapClientTests.cs index 90069a0a8d..f45f1c2e16 100644 --- a/UnitTests/Net/Imap/ImapClientTests.cs +++ b/UnitTests/Net/Imap/ImapClientTests.cs @@ -1,9 +1,9 @@ -// +// // ImapClientTests.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -24,28 +24,30 @@ // THE SOFTWARE. // -using System; -using System.IO; using System.Net; -using System.Linq; using System.Text; -using System.Collections.Generic; -using System.Security.Cryptography; - -using NUnit.Framework; +using System.Net.Sockets; +using System.Net.Security; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; using MimeKit; -using MailKit.Net.Imap; -using MailKit.Security; -using MailKit.Search; using MailKit; +using MailKit.Search; +using MailKit.Security; +using MailKit.Net.Imap; +using MailKit.Net.Proxy; + +using UnitTests.Security; +using UnitTests.Net.Proxy; + +using AuthenticationException = MailKit.Security.AuthenticationException; namespace UnitTests.Net.Imap { [TestFixture] public class ImapClientTests { - static readonly Encoding Latin1 = Encoding.GetEncoding (28591); static readonly ImapCapabilities GreetingCapabilities = ImapCapabilities.IMAP4rev1 | ImapCapabilities.Status | ImapCapabilities.Namespace | ImapCapabilities.Unselect; static readonly ImapCapabilities DovecotInitialCapabilities = ImapCapabilities.IMAP4rev1 | ImapCapabilities.Status | @@ -69,93 +71,146 @@ public class ImapClientTests ImapCapabilities.ESearch | ImapCapabilities.Compress | ImapCapabilities.Enable | ImapCapabilities.ListExtended | ImapCapabilities.ListStatus | ImapCapabilities.Move | ImapCapabilities.UTF8Accept | ImapCapabilities.XList | ImapCapabilities.GMailExt1 | ImapCapabilities.LiteralMinus | ImapCapabilities.AppendLimit; + static readonly ImapCapabilities ICloudInitialCapabilities = ImapCapabilities.IMAP4 | ImapCapabilities.IMAP4rev1 | + ImapCapabilities.Status | ImapCapabilities.SaslIR; + static readonly ImapCapabilities ICloudAuthenticatedCapabilities = ImapCapabilities.IMAP4 | ImapCapabilities.IMAP4rev1 | + ImapCapabilities.Status | ImapCapabilities.CondStore | ImapCapabilities.Enable | ImapCapabilities.QuickResync | + ImapCapabilities.Quota | ImapCapabilities.Namespace | ImapCapabilities.UidPlus | ImapCapabilities.Children | + ImapCapabilities.Binary | ImapCapabilities.Unselect | ImapCapabilities.Sort | ImapCapabilities.Catenate | + ImapCapabilities.Language | ImapCapabilities.ESearch | ImapCapabilities.ESort | ImapCapabilities.Thread | + ImapCapabilities.Context | ImapCapabilities.Within | ImapCapabilities.SaslIR | ImapCapabilities.SearchResults | + ImapCapabilities.Metadata | ImapCapabilities.Id | ImapCapabilities.Annotate | ImapCapabilities.MultiSearch | + ImapCapabilities.Idle | ImapCapabilities.ListStatus; + static readonly ImapCapabilities IMAP4rev2CoreCapabilities = ImapCapabilities.IMAP4rev2 | ImapCapabilities.Status | + ImapCapabilities.Namespace | ImapCapabilities.Unselect | ImapCapabilities.UidPlus | ImapCapabilities.ESearch | + ImapCapabilities.SearchResults | ImapCapabilities.Enable | ImapCapabilities.Idle | ImapCapabilities.SaslIR | ImapCapabilities.ListExtended | + ImapCapabilities.ListStatus | ImapCapabilities.Move | ImapCapabilities.LiteralMinus | ImapCapabilities.SpecialUse; static readonly ImapCapabilities AclInitialCapabilities = GMailInitialCapabilities | ImapCapabilities.Acl; static readonly ImapCapabilities AclAuthenticatedCapabilities = GMailAuthenticatedCapabilities | ImapCapabilities.Acl; static readonly ImapCapabilities MetadataInitialCapabilities = GMailInitialCapabilities | ImapCapabilities.Metadata; static readonly ImapCapabilities MetadataAuthenticatedCapabilities = GMailAuthenticatedCapabilities | ImapCapabilities.Metadata; + const CipherAlgorithmType GmxDeCipherAlgorithm = CipherAlgorithmType.Aes256; + const int GmxDeCipherStrength = 256; +#if !MONO + const HashAlgorithmType GmxDeHashAlgorithm = HashAlgorithmType.Sha384; +#else + const HashAlgorithmType GmxDeHashAlgorithm = HashAlgorithmType.None; +#endif + const ExchangeAlgorithmType EcdhEphemeral = (ExchangeAlgorithmType) 44550; static FolderAttributes GetSpecialFolderAttribute (SpecialFolder special) { switch (special) { - case SpecialFolder.All: return FolderAttributes.All; - case SpecialFolder.Archive: return FolderAttributes.Archive; - case SpecialFolder.Drafts: return FolderAttributes.Drafts; - case SpecialFolder.Flagged: return FolderAttributes.Flagged; - case SpecialFolder.Junk: return FolderAttributes.Junk; - case SpecialFolder.Sent: return FolderAttributes.Sent; - case SpecialFolder.Trash: return FolderAttributes.Trash; - default: throw new ArgumentOutOfRangeException (); + case SpecialFolder.All: return FolderAttributes.All; + case SpecialFolder.Archive: return FolderAttributes.Archive; + case SpecialFolder.Drafts: return FolderAttributes.Drafts; + case SpecialFolder.Flagged: return FolderAttributes.Flagged; + case SpecialFolder.Important: return FolderAttributes.Important; + case SpecialFolder.Junk: return FolderAttributes.Junk; + case SpecialFolder.Sent: return FolderAttributes.Sent; + case SpecialFolder.Trash: return FolderAttributes.Trash; + default: throw new ArgumentOutOfRangeException (nameof (special)); } } - Stream GetResourceStream (string name) + static Stream GetResourceStream (string name) { - return GetType ().Assembly.GetManifestResourceStream ("UnitTests.Net.Imap.Resources." + name); + return typeof (ImapClientTests).Assembly.GetManifestResourceStream ("UnitTests.Net.Imap.Resources." + name); } - static string HexEncode (byte[] digest) + static void GetStreamsCallback (ImapFolder folder, int index, UniqueId uid, Stream stream) { - var hex = new StringBuilder (); + using (var reader = new StreamReader (stream)) { + const string expected = "This is some dummy text just to make sure this is working correctly."; + var text = reader.ReadToEnd (); - for (int i = 0; i < digest.Length; i++) - hex.Append (digest[i].ToString ("x2")); + Assert.That (text, Is.EqualTo (expected)); + } + } - return hex.ToString (); + static async Task GetStreamsAsyncCallback (ImapFolder folder, int index, UniqueId uid, Stream stream, CancellationToken cancellationToken) + { + using (var reader = new StreamReader (stream)) { + const string expected = "This is some dummy text just to make sure this is working correctly."; +#if NET8_0_OR_GREATER + var text = await reader.ReadToEndAsync (cancellationToken); +#else + var text = await reader.ReadToEndAsync (); +#endif + + Assert.That (text, Is.EqualTo (expected)); + } } [Test] public void TestArgumentExceptions () { - var commands = new List (); - commands.Add (new ImapReplayCommand ("", "dovecot.greeting.txt")); - commands.Add (new ImapReplayCommand ("A00000000 LOGIN username password\r\n", "dovecot.authenticate.txt")); - commands.Add (new ImapReplayCommand ("A00000001 NAMESPACE\r\n", "dovecot.namespace.txt")); - commands.Add (new ImapReplayCommand ("A00000002 LIST \"\" \"INBOX\"\r\n", "dovecot.list-inbox.txt")); - commands.Add (new ImapReplayCommand ("A00000003 LIST (SPECIAL-USE) \"\" \"*\"\r\n", "dovecot.list-special-use.txt")); - commands.Add (new ImapReplayCommand ("A00000004 SELECT INBOX (CONDSTORE)\r\n", "common.select-inbox.txt")); - - using (var client = new ImapClient ()) { + var commands = new List { + new ImapReplayCommand ("", "dovecot.greeting.txt"), + new ImapReplayCommand ("A00000000 LOGIN username password\r\n", "dovecot.authenticate+gmail-capabilities.txt"), + new ImapReplayCommand ("A00000001 NAMESPACE\r\n", "dovecot.namespace.txt"), + new ImapReplayCommand ("A00000002 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-inbox.txt"), + new ImapReplayCommand ("A00000003 LIST (SPECIAL-USE) \"\" \"*\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-special-use.txt") + }; + + using (var client = new ImapClient () { TagPrefix = 'A' }) { var credentials = new NetworkCredential ("username", "password"); + Assert.That (client.SyncRoot, Is.InstanceOf (), "SyncRoot"); + // Connect Assert.Throws (() => client.Connect ((Uri) null)); - Assert.Throws (async () => await client.ConnectAsync ((Uri) null)); + Assert.ThrowsAsync (async () => await client.ConnectAsync ((Uri) null)); + Assert.Throws (() => client.Connect (new Uri ("path", UriKind.Relative))); + Assert.ThrowsAsync (async () => await client.ConnectAsync (new Uri ("path", UriKind.Relative))); Assert.Throws (() => client.Connect (null, 143, false)); - Assert.Throws (async () => await client.ConnectAsync (null, 143, false)); + Assert.ThrowsAsync (async () => await client.ConnectAsync (null, 143, false)); Assert.Throws (() => client.Connect (string.Empty, 143, false)); - Assert.Throws (async () => await client.ConnectAsync (string.Empty, 143, false)); + Assert.ThrowsAsync (async () => await client.ConnectAsync (string.Empty, 143, false)); Assert.Throws (() => client.Connect ("host", -1, false)); - Assert.Throws (async () => await client.ConnectAsync ("host", -1, false)); + Assert.ThrowsAsync (async () => await client.ConnectAsync ("host", -1, false)); Assert.Throws (() => client.Connect (null, 143, SecureSocketOptions.None)); - Assert.Throws (async () => await client.ConnectAsync (null, 143, SecureSocketOptions.None)); + Assert.ThrowsAsync (async () => await client.ConnectAsync (null, 143, SecureSocketOptions.None)); Assert.Throws (() => client.Connect (string.Empty, 143, SecureSocketOptions.None)); - Assert.Throws (async () => await client.ConnectAsync (string.Empty, 143, SecureSocketOptions.None)); + Assert.ThrowsAsync (async () => await client.ConnectAsync (string.Empty, 143, SecureSocketOptions.None)); Assert.Throws (() => client.Connect ("host", -1, SecureSocketOptions.None)); - Assert.Throws (async () => await client.ConnectAsync ("host", -1, SecureSocketOptions.None)); + Assert.ThrowsAsync (async () => await client.ConnectAsync ("host", -1, SecureSocketOptions.None)); + + Assert.Throws (() => client.Connect ((Socket) null, "host", 143, SecureSocketOptions.None)); + Assert.ThrowsAsync (async () => await client.ConnectAsync ((Socket) null, "host", 143, SecureSocketOptions.None)); + Assert.Throws (() => client.Connect ((Stream) null, "host", 143, SecureSocketOptions.None)); + Assert.ThrowsAsync (async () => await client.ConnectAsync ((Stream) null, "host", 143, SecureSocketOptions.None)); + + using (var socket = new Socket (AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) { + Assert.Throws (() => client.Connect (socket, "host", 143, SecureSocketOptions.None)); + Assert.ThrowsAsync (async () => await client.ConnectAsync (socket, "host", 143, SecureSocketOptions.None)); + } try { - client.ReplayConnect ("localhost", new ImapReplayStream (commands, false)); + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); } catch (Exception ex) { - Assert.Fail ("Did not expect an exception in Connect: {0}", ex); + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); } // Authenticate - Assert.Throws (() => client.Authenticate (null)); - Assert.Throws (async () => await client.AuthenticateAsync (null)); + Assert.Throws (() => client.Authenticate ((SaslMechanism) null)); + Assert.ThrowsAsync (async () => await client.AuthenticateAsync ((SaslMechanism) null)); + Assert.Throws (() => client.Authenticate ((ICredentials) null)); + Assert.ThrowsAsync (async () => await client.AuthenticateAsync ((ICredentials) null)); Assert.Throws (() => client.Authenticate (null, "password")); - Assert.Throws (async () => await client.AuthenticateAsync (null, "password")); + Assert.ThrowsAsync (async () => await client.AuthenticateAsync (null, "password")); Assert.Throws (() => client.Authenticate ("username", null)); - Assert.Throws (async () => await client.AuthenticateAsync ("username", null)); + Assert.ThrowsAsync (async () => await client.AuthenticateAsync ("username", null)); Assert.Throws (() => client.Authenticate (null, credentials)); - Assert.Throws (async () => await client.AuthenticateAsync (null, credentials)); + Assert.ThrowsAsync (async () => await client.AuthenticateAsync (null, credentials)); Assert.Throws (() => client.Authenticate (Encoding.UTF8, null)); - Assert.Throws (async () => await client.AuthenticateAsync (Encoding.UTF8, null)); + Assert.ThrowsAsync (async () => await client.AuthenticateAsync (Encoding.UTF8, null)); Assert.Throws (() => client.Authenticate (null, "username", "password")); - Assert.Throws (async () => await client.AuthenticateAsync (null, "username", "password")); + Assert.ThrowsAsync (async () => await client.AuthenticateAsync (null, "username", "password")); Assert.Throws (() => client.Authenticate (Encoding.UTF8, null, "password")); - Assert.Throws (async () => await client.AuthenticateAsync (Encoding.UTF8, null, "password")); + Assert.ThrowsAsync (async () => await client.AuthenticateAsync (Encoding.UTF8, null, "password")); Assert.Throws (() => client.Authenticate (Encoding.UTF8, "username", null)); - Assert.Throws (async () => await client.AuthenticateAsync (Encoding.UTF8, "username", null)); + Assert.ThrowsAsync (async () => await client.AuthenticateAsync (Encoding.UTF8, "username", null)); // Note: we do not want to use SASL at all... client.AuthenticationMechanisms.Clear (); @@ -163,1945 +218,6990 @@ public void TestArgumentExceptions () try { client.Authenticate (credentials); } catch (Exception ex) { - Assert.Fail ("Did not expect an exception in Authenticate: {0}", ex); + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); } + // Notify + Assert.Throws (() => client.Notify (true, null)); + Assert.ThrowsAsync (async () => await client.NotifyAsync (true, null)); + Assert.Throws (() => client.Notify (true, Array.Empty ())); + Assert.ThrowsAsync (async () => await client.NotifyAsync (true, Array.Empty ())); + + Assert.Throws (() => new ImapMailboxFilter.Subtree (client.Inbox, null)); + Assert.Throws (() => new ImapMailboxFilter.Mailboxes (client.Inbox, null)); + Assert.Throws (() => client.GetFolder ((string) null)); Assert.Throws (() => client.GetFolder ((FolderNamespace) null)); + Assert.ThrowsAsync (async () => await client.GetFolderAsync ((string) null)); Assert.Throws (() => client.GetFolders (null)); Assert.Throws (() => client.GetFolders (null, false)); - Assert.Throws (async () => await client.GetFoldersAsync (null)); - Assert.Throws (async () => await client.GetFoldersAsync (null, false)); - - var personal = client.GetFolder (client.PersonalNamespaces[0]); - var dates = new List (); - var messages = new List (); - var flags = new List (); - var now = DateTimeOffset.Now; - - messages.Add (CreateThreadableMessage ("A", "", null, now.AddMinutes (-7))); - messages.Add (CreateThreadableMessage ("B", "", "", now.AddMinutes (-6))); - messages.Add (CreateThreadableMessage ("C", "", " ", now.AddMinutes (-5))); - messages.Add (CreateThreadableMessage ("D", "", "", now.AddMinutes (-4))); - messages.Add (CreateThreadableMessage ("E", "", " ", now.AddMinutes (-3))); - messages.Add (CreateThreadableMessage ("F", "", "", now.AddMinutes (-2))); - messages.Add (CreateThreadableMessage ("G", "", null, now.AddMinutes (-1))); - messages.Add (CreateThreadableMessage ("H", "", null, now)); - - for (int i = 0; i < messages.Count; i++) { - dates.Add (DateTimeOffset.Now); - flags.Add (MessageFlags.Seen); - } - - var inbox = (ImapFolder) client.Inbox; - inbox.Open (FolderAccess.ReadWrite); - - // ImapFolder .ctor - Assert.Throws (() => new ImapFolder (null)); - - // Open - Assert.Throws (() => inbox.Open ((FolderAccess) 500)); - Assert.Throws (() => inbox.Open ((FolderAccess) 500, 0, 0, UniqueIdRange.All)); - Assert.Throws (async () => await inbox.OpenAsync ((FolderAccess) 500)); - Assert.Throws (async () => await inbox.OpenAsync ((FolderAccess) 500, 0, 0, UniqueIdRange.All)); - - // Create - Assert.Throws (() => inbox.Create (null, true)); - Assert.Throws (() => inbox.Create (string.Empty, true)); - Assert.Throws (() => inbox.Create ("Folder./Name", true)); - Assert.Throws (() => inbox.Create (null, SpecialFolder.All)); - Assert.Throws (() => inbox.Create (string.Empty, SpecialFolder.All)); - Assert.Throws (() => inbox.Create ("Folder./Name", SpecialFolder.All)); - Assert.Throws (() => inbox.Create (null, new SpecialFolder[] { SpecialFolder.All })); - Assert.Throws (() => inbox.Create (string.Empty, new SpecialFolder[] { SpecialFolder.All })); - Assert.Throws (() => inbox.Create ("Folder./Name", new SpecialFolder[] { SpecialFolder.All })); - Assert.Throws (() => inbox.Create ("ValidName", null)); - Assert.Throws (() => inbox.Create ("ValidName", SpecialFolder.All)); - Assert.Throws (async () => await inbox.CreateAsync (null, true)); - Assert.Throws (async () => await inbox.CreateAsync (string.Empty, true)); - Assert.Throws (async () => await inbox.CreateAsync ("Folder./Name", true)); - Assert.Throws (async () => await inbox.CreateAsync (null, SpecialFolder.All)); - Assert.Throws (async () => await inbox.CreateAsync (string.Empty, SpecialFolder.All)); - Assert.Throws (async () => await inbox.CreateAsync ("Folder./Name", SpecialFolder.All)); - Assert.Throws (async () => await inbox.CreateAsync (null, new SpecialFolder[] { SpecialFolder.All })); - Assert.Throws (async () => await inbox.CreateAsync (string.Empty, new SpecialFolder[] { SpecialFolder.All })); - Assert.Throws (async () => await inbox.CreateAsync ("Folder./Name", new SpecialFolder[] { SpecialFolder.All })); - Assert.Throws (async () => await inbox.CreateAsync ("ValidName", null)); - Assert.Throws (async () => await inbox.CreateAsync ("ValidName", SpecialFolder.All)); - - // Rename - Assert.Throws (() => inbox.Rename (null, "NewName")); - Assert.Throws (() => inbox.Rename (personal, null)); - Assert.Throws (() => inbox.Rename (personal, string.Empty)); - Assert.Throws (async () => await inbox.RenameAsync (null, "NewName")); - Assert.Throws (async () => await inbox.RenameAsync (personal, null)); - Assert.Throws (async () => await inbox.RenameAsync (personal, string.Empty)); - - // GetSubfolder - Assert.Throws (() => inbox.GetSubfolder (null)); - Assert.Throws (() => inbox.GetSubfolder (string.Empty)); - Assert.Throws (async () => await inbox.GetSubfolderAsync (null)); - Assert.Throws (async () => await inbox.GetSubfolderAsync (string.Empty)); - - // GetMetadata - Assert.Throws (() => client.GetMetadata (null, new MetadataTag[] { MetadataTag.PrivateComment })); - Assert.Throws (() => client.GetMetadata (new MetadataOptions (), null)); - Assert.Throws (async () => await client.GetMetadataAsync (null, new MetadataTag[] { MetadataTag.PrivateComment })); - Assert.Throws (async () => await client.GetMetadataAsync (new MetadataOptions (), null)); - Assert.Throws (() => inbox.GetMetadata (null, new MetadataTag[] { MetadataTag.PrivateComment })); - Assert.Throws (() => inbox.GetMetadata (new MetadataOptions (), null)); - Assert.Throws (async () => await inbox.GetMetadataAsync (null, new MetadataTag[] { MetadataTag.PrivateComment })); - Assert.Throws (async () => await inbox.GetMetadataAsync (new MetadataOptions (), null)); - - // SetMetadata - Assert.Throws (() => client.SetMetadata (null)); - Assert.Throws (async () => await client.SetMetadataAsync (null)); - Assert.Throws (() => inbox.SetMetadata (null)); - Assert.Throws (async () => await inbox.SetMetadataAsync (null)); - - // Expunge - Assert.Throws (() => inbox.Expunge (null)); - Assert.Throws (async () => await inbox.ExpungeAsync (null)); - - // Append - Assert.Throws (() => inbox.Append (null)); - Assert.Throws (async () => await inbox.AppendAsync (null)); - Assert.Throws (() => inbox.Append (null, messages[0])); - Assert.Throws (async () => await inbox.AppendAsync (null, messages[0])); - Assert.Throws (() => inbox.Append (FormatOptions.Default, null)); - Assert.Throws (async () => await inbox.AppendAsync (FormatOptions.Default, null)); - Assert.Throws (() => inbox.Append (null, MessageFlags.None, DateTimeOffset.Now)); - Assert.Throws (async () => await inbox.AppendAsync (null, MessageFlags.None, DateTimeOffset.Now)); - Assert.Throws (() => inbox.Append (null, messages[0], MessageFlags.None, DateTimeOffset.Now)); - Assert.Throws (async () => await inbox.AppendAsync (null, messages[0], MessageFlags.None, DateTimeOffset.Now)); - Assert.Throws (() => inbox.Append (FormatOptions.Default, null, MessageFlags.None, DateTimeOffset.Now)); - Assert.Throws (async () => await inbox.AppendAsync (FormatOptions.Default, null, MessageFlags.None, DateTimeOffset.Now)); - - // MultiAppend - Assert.Throws (() => inbox.Append (null, flags)); - Assert.Throws (async () => await inbox.AppendAsync (null, flags)); - Assert.Throws (() => inbox.Append (messages, null)); - Assert.Throws (async () => await inbox.AppendAsync (messages, null)); - Assert.Throws (() => inbox.Append (null, messages, flags)); - Assert.Throws (async () => await inbox.AppendAsync (null, messages, flags)); - Assert.Throws (() => inbox.Append (FormatOptions.Default, null, flags)); - Assert.Throws (async () => await inbox.AppendAsync (FormatOptions.Default, null, flags)); - Assert.Throws (() => inbox.Append (FormatOptions.Default, messages, null)); - Assert.Throws (async () => await inbox.AppendAsync (FormatOptions.Default, messages, null)); - Assert.Throws (() => inbox.Append (null, flags, dates)); - Assert.Throws (async () => await inbox.AppendAsync (null, flags, dates)); - Assert.Throws (() => inbox.Append (messages, null, dates)); - Assert.Throws (async () => await inbox.AppendAsync (messages, null, dates)); - Assert.Throws (() => inbox.Append (messages, flags, null)); - Assert.Throws (async () => await inbox.AppendAsync (messages, flags, null)); - Assert.Throws (() => inbox.Append (null, messages, flags, dates)); - Assert.Throws (async () => await inbox.AppendAsync (null, messages, flags, dates)); - Assert.Throws (() => inbox.Append (FormatOptions.Default, null, flags, dates)); - Assert.Throws (async () => await inbox.AppendAsync (FormatOptions.Default, null, flags, dates)); - Assert.Throws (() => inbox.Append (FormatOptions.Default, messages, null, dates)); - Assert.Throws (async () => await inbox.AppendAsync (FormatOptions.Default, messages, null, dates)); - Assert.Throws (() => inbox.Append (FormatOptions.Default, messages, flags, null)); - Assert.Throws (async () => await inbox.AppendAsync (FormatOptions.Default, messages, flags, null)); - - // CopyTo - Assert.Throws (() => inbox.CopyTo ((IList) null, inbox)); - Assert.Throws (async () => await inbox.CopyToAsync ((IList) null, inbox)); - Assert.Throws (() => inbox.CopyTo (UniqueIdRange.All, null)); - Assert.Throws (async () => await inbox.CopyToAsync (UniqueIdRange.All, null)); - Assert.Throws (() => inbox.CopyTo ((IList) null, inbox)); - Assert.Throws (async () => await inbox.CopyToAsync ((IList) null, inbox)); - Assert.Throws (() => inbox.CopyTo (new int[] { 0 }, null)); - Assert.Throws (async () => await inbox.CopyToAsync (new int[] { 0 }, null)); - - // MoveTo - Assert.Throws (() => inbox.MoveTo ((IList) null, inbox)); - Assert.Throws (async () => await inbox.MoveToAsync ((IList) null, inbox)); - Assert.Throws (() => inbox.MoveTo (UniqueIdRange.All, null)); - Assert.Throws (async () => await inbox.MoveToAsync (UniqueIdRange.All, null)); - Assert.Throws (() => inbox.MoveTo ((IList) null, inbox)); - Assert.Throws (async () => await inbox.MoveToAsync ((IList) null, inbox)); - Assert.Throws (() => inbox.MoveTo (new int[] { 0 }, null)); - Assert.Throws (async () => await inbox.MoveToAsync (new int[] { 0 }, null)); - - // Fetch - var headers = new HashSet (new HeaderId[] { HeaderId.Subject }); - var fields = new HashSet (new string[] { "SUBJECT" }); - var uids = new UniqueId[] { UniqueId.MinValue }; - var emptyHeaders = new HashSet (); - var emptyFields = new HashSet (); - var indexes = new int[] { 0 }; - - Assert.Throws (() => inbox.Fetch (-1, -1, MessageSummaryItems.All)); - Assert.Throws (async () => await inbox.FetchAsync (-1, -1, MessageSummaryItems.All)); - Assert.Throws (() => inbox.Fetch (5, 1, MessageSummaryItems.All)); - Assert.Throws (async () => await inbox.FetchAsync (5, 1, MessageSummaryItems.All)); - Assert.Throws (() => inbox.Fetch (0, 5, MessageSummaryItems.None)); - Assert.Throws (async () => await inbox.FetchAsync (0, 5, MessageSummaryItems.None)); - - Assert.Throws (() => inbox.Fetch ((IList) null, MessageSummaryItems.All)); - Assert.Throws (async () => await inbox.FetchAsync ((IList) null, MessageSummaryItems.All)); - Assert.Throws (() => inbox.Fetch (uids, MessageSummaryItems.None)); - Assert.Throws (async () => await inbox.FetchAsync (uids, MessageSummaryItems.None)); - - Assert.Throws (() => inbox.Fetch ((IList) null, MessageSummaryItems.All)); - Assert.Throws (async () => await inbox.FetchAsync ((IList) null, MessageSummaryItems.All)); - Assert.Throws (() => inbox.Fetch (indexes, MessageSummaryItems.None)); - Assert.Throws (async () => await inbox.FetchAsync (indexes, MessageSummaryItems.None)); - - Assert.Throws (() => inbox.Fetch (-1, -1, MessageSummaryItems.All, headers)); - Assert.Throws (async () => await inbox.FetchAsync (-1, -1, MessageSummaryItems.All, headers)); - Assert.Throws (() => inbox.Fetch (5, 1, MessageSummaryItems.All, headers)); - Assert.Throws (async () => await inbox.FetchAsync (5, 1, MessageSummaryItems.All, headers)); - //Assert.Throws (() => inbox.Fetch (0, 5, MessageSummaryItems.None, headers)); - //Assert.Throws (async () => await inbox.FetchAsync (0, 5, MessageSummaryItems.None, headers)); - Assert.Throws (() => inbox.Fetch (0, 5, MessageSummaryItems.All, (HashSet) null)); - Assert.Throws (async () => await inbox.FetchAsync (0, 5, MessageSummaryItems.All, (HashSet) null)); - Assert.Throws (() => inbox.Fetch (0, 5, MessageSummaryItems.All, emptyHeaders)); - Assert.Throws (async () => await inbox.FetchAsync (0, 5, MessageSummaryItems.All, emptyHeaders)); - - Assert.Throws (() => inbox.Fetch ((IList) null, MessageSummaryItems.All, headers)); - Assert.Throws (async () => await inbox.FetchAsync ((IList) null, MessageSummaryItems.All, headers)); - //Assert.Throws (() => inbox.Fetch (uids, MessageSummaryItems.None, headers)); - //Assert.Throws (async () => await inbox.FetchAsync (uids, MessageSummaryItems.None, headers)); - Assert.Throws (() => inbox.Fetch (uids, MessageSummaryItems.All, (HashSet) null)); - Assert.Throws (async () => await inbox.FetchAsync (uids, MessageSummaryItems.All, (HashSet) null)); - Assert.Throws (() => inbox.Fetch (uids, MessageSummaryItems.All, emptyHeaders)); - Assert.Throws (async () => await inbox.FetchAsync (uids, MessageSummaryItems.All, emptyHeaders)); - - Assert.Throws (() => inbox.Fetch ((IList) null, MessageSummaryItems.All, headers)); - Assert.Throws (async () => await inbox.FetchAsync ((IList) null, MessageSummaryItems.All, headers)); - //Assert.Throws (() => inbox.Fetch (indexes, MessageSummaryItems.None, headers)); - //Assert.Throws (async () => await inbox.FetchAsync (indexes, MessageSummaryItems.None, headers)); - Assert.Throws (() => inbox.Fetch (indexes, MessageSummaryItems.All, (HashSet) null)); - Assert.Throws (async () => await inbox.FetchAsync (indexes, MessageSummaryItems.All, (HashSet) null)); - Assert.Throws (() => inbox.Fetch (indexes, MessageSummaryItems.All, emptyHeaders)); - Assert.Throws (async () => await inbox.FetchAsync (indexes, MessageSummaryItems.All, emptyHeaders)); - - Assert.Throws (() => inbox.Fetch (-1, -1, MessageSummaryItems.All, fields)); - Assert.Throws (async () => await inbox.FetchAsync (-1, -1, MessageSummaryItems.All, fields)); - Assert.Throws (() => inbox.Fetch (5, 1, MessageSummaryItems.All, fields)); - Assert.Throws (async () => await inbox.FetchAsync (5, 1, MessageSummaryItems.All, fields)); - //Assert.Throws (() => inbox.Fetch (0, 5, MessageSummaryItems.None, fields)); - //Assert.Throws (async () => await inbox.FetchAsync (0, 5, MessageSummaryItems.None, fields)); - Assert.Throws (() => inbox.Fetch (0, 5, MessageSummaryItems.All, (HashSet) null)); - Assert.Throws (async () => await inbox.FetchAsync (0, 5, MessageSummaryItems.All, (HashSet) null)); - Assert.Throws (() => inbox.Fetch (0, 5, MessageSummaryItems.All, emptyFields)); - Assert.Throws (async () => await inbox.FetchAsync (0, 5, MessageSummaryItems.All, emptyFields)); - - Assert.Throws (() => inbox.Fetch ((IList) null, MessageSummaryItems.All, fields)); - Assert.Throws (async () => await inbox.FetchAsync ((IList) null, MessageSummaryItems.All, fields)); - //Assert.Throws (() => inbox.Fetch (uids, MessageSummaryItems.None, fields)); - //Assert.Throws (async () => await inbox.FetchAsync (uids, MessageSummaryItems.None, fields)); - Assert.Throws (() => inbox.Fetch (uids, MessageSummaryItems.All, (HashSet) null)); - Assert.Throws (async () => await inbox.FetchAsync (uids, MessageSummaryItems.All, (HashSet) null)); - Assert.Throws (() => inbox.Fetch (uids, MessageSummaryItems.All, emptyFields)); - Assert.Throws (async () => await inbox.FetchAsync (uids, MessageSummaryItems.All, emptyFields)); - - Assert.Throws (() => inbox.Fetch ((IList) null, MessageSummaryItems.All, fields)); - Assert.Throws (async () => await inbox.FetchAsync ((IList) null, MessageSummaryItems.All, fields)); - //Assert.Throws (() => inbox.Fetch (indexes, MessageSummaryItems.None, fields)); - //Assert.Throws (async () => await inbox.FetchAsync (indexes, MessageSummaryItems.None, fields)); - Assert.Throws (() => inbox.Fetch (indexes, MessageSummaryItems.All, (HashSet) null)); - Assert.Throws (async () => await inbox.FetchAsync (indexes, MessageSummaryItems.All, (HashSet) null)); - Assert.Throws (() => inbox.Fetch (indexes, MessageSummaryItems.All, emptyFields)); - Assert.Throws (async () => await inbox.FetchAsync (indexes, MessageSummaryItems.All, emptyFields)); + Assert.ThrowsAsync (async () => await client.GetFoldersAsync (null)); + Assert.ThrowsAsync (async () => await client.GetFoldersAsync (null, false)); - // Fetch + modseq - Assert.Throws (() => inbox.Fetch (-1, -1, 31337, MessageSummaryItems.All)); - Assert.Throws (async () => await inbox.FetchAsync (-1, -1, 31337, MessageSummaryItems.All)); - Assert.Throws (() => inbox.Fetch (5, 1, 31337, MessageSummaryItems.All)); - Assert.Throws (async () => await inbox.FetchAsync (5, 1, 31337, MessageSummaryItems.All)); - Assert.Throws (() => inbox.Fetch (0, 5, 31337, MessageSummaryItems.None)); - Assert.Throws (async () => await inbox.FetchAsync (0, 5, 31337, MessageSummaryItems.None)); - - Assert.Throws (() => inbox.Fetch ((IList) null, 31337, MessageSummaryItems.All)); - Assert.Throws (async () => await inbox.FetchAsync ((IList) null, 31337, MessageSummaryItems.All)); - Assert.Throws (() => inbox.Fetch (uids, 31337, MessageSummaryItems.None)); - Assert.Throws (async () => await inbox.FetchAsync (uids, 31337, MessageSummaryItems.None)); - - Assert.Throws (() => inbox.Fetch ((IList) null, 31337, MessageSummaryItems.All)); - Assert.Throws (async () => await inbox.FetchAsync ((IList) null, 31337, MessageSummaryItems.All)); - Assert.Throws (() => inbox.Fetch (indexes, 31337, MessageSummaryItems.None)); - Assert.Throws (async () => await inbox.FetchAsync (indexes, 31337, MessageSummaryItems.None)); - - Assert.Throws (() => inbox.Fetch (-1, -1, 31337, MessageSummaryItems.All, headers)); - Assert.Throws (async () => await inbox.FetchAsync (-1, -1, 31337, MessageSummaryItems.All, headers)); - Assert.Throws (() => inbox.Fetch (5, 1, 31337, MessageSummaryItems.All, headers)); - Assert.Throws (async () => await inbox.FetchAsync (5, 1, MessageSummaryItems.All, headers)); - //Assert.Throws (() => inbox.Fetch (0, 5, 31337, MessageSummaryItems.None, headers)); - //Assert.Throws (async () => await inbox.FetchAsync (0, 5, 31337, MessageSummaryItems.None, headers)); - Assert.Throws (() => inbox.Fetch (0, 5, 31337, MessageSummaryItems.All, (HashSet) null)); - Assert.Throws (async () => await inbox.FetchAsync (0, 5, 31337, MessageSummaryItems.All, (HashSet) null)); - Assert.Throws (() => inbox.Fetch (0, 5, 31337, MessageSummaryItems.All, emptyHeaders)); - Assert.Throws (async () => await inbox.FetchAsync (0, 5, 31337, MessageSummaryItems.All, emptyHeaders)); - - Assert.Throws (() => inbox.Fetch ((IList) null, 31337, MessageSummaryItems.All, headers)); - Assert.Throws (async () => await inbox.FetchAsync ((IList) null, 31337, MessageSummaryItems.All, headers)); - //Assert.Throws (() => inbox.Fetch (uids, 31337, MessageSummaryItems.None, headers)); - //Assert.Throws (async () => await inbox.FetchAsync (uids, 31337, MessageSummaryItems.None, headers)); - Assert.Throws (() => inbox.Fetch (uids, 31337, MessageSummaryItems.All, (HashSet) null)); - Assert.Throws (async () => await inbox.FetchAsync (uids, 31337, MessageSummaryItems.All, (HashSet) null)); - Assert.Throws (() => inbox.Fetch (uids, 31337, MessageSummaryItems.All, emptyHeaders)); - Assert.Throws (async () => await inbox.FetchAsync (uids, 31337, MessageSummaryItems.All, emptyHeaders)); - - Assert.Throws (() => inbox.Fetch ((IList) null, 31337, MessageSummaryItems.All, headers)); - Assert.Throws (async () => await inbox.FetchAsync ((IList) null, 31337, MessageSummaryItems.All, headers)); - //Assert.Throws (() => inbox.Fetch (indexes, 31337, MessageSummaryItems.None, headers)); - //Assert.Throws (async () => await inbox.FetchAsync (indexes, 31337, MessageSummaryItems.None, headers)); - Assert.Throws (() => inbox.Fetch (indexes, 31337, MessageSummaryItems.All, (HashSet) null)); - Assert.Throws (async () => await inbox.FetchAsync (indexes, 31337, MessageSummaryItems.All, (HashSet) null)); - Assert.Throws (() => inbox.Fetch (indexes, 31337, MessageSummaryItems.All, emptyHeaders)); - Assert.Throws (async () => await inbox.FetchAsync (indexes, 31337, MessageSummaryItems.All, emptyHeaders)); - - Assert.Throws (() => inbox.Fetch (-1, -1, 31337, MessageSummaryItems.All, fields)); - Assert.Throws (async () => await inbox.FetchAsync (-1, -1, 31337, MessageSummaryItems.All, fields)); - Assert.Throws (() => inbox.Fetch (5, 1, 31337, MessageSummaryItems.All, fields)); - Assert.Throws (async () => await inbox.FetchAsync (5, 1, 31337, MessageSummaryItems.All, fields)); - //Assert.Throws (() => inbox.Fetch (0, 5, 31337, MessageSummaryItems.None, fields)); - //Assert.Throws (async () => await inbox.FetchAsync (0, 5, 31337, MessageSummaryItems.None, fields)); - Assert.Throws (() => inbox.Fetch (0, 5, 31337, MessageSummaryItems.All, (HashSet) null)); - Assert.Throws (async () => await inbox.FetchAsync (0, 5, 31337, MessageSummaryItems.All, (HashSet) null)); - Assert.Throws (() => inbox.Fetch (0, 5, 31337, MessageSummaryItems.All, emptyFields)); - Assert.Throws (async () => await inbox.FetchAsync (0, 5, 31337, MessageSummaryItems.All, emptyFields)); - - Assert.Throws (() => inbox.Fetch ((IList) null, 31337, MessageSummaryItems.All, fields)); - Assert.Throws (async () => await inbox.FetchAsync ((IList) null, 31337, MessageSummaryItems.All, fields)); - //Assert.Throws (() => inbox.Fetch (uids, 31337, MessageSummaryItems.None, fields)); - //Assert.Throws (async () => await inbox.FetchAsync (uids, 31337, MessageSummaryItems.None, fields)); - Assert.Throws (() => inbox.Fetch (uids, 31337, MessageSummaryItems.All, (HashSet) null)); - Assert.Throws (async () => await inbox.FetchAsync (uids, 31337, MessageSummaryItems.All, (HashSet) null)); - Assert.Throws (() => inbox.Fetch (uids, 31337, MessageSummaryItems.All, emptyFields)); - Assert.Throws (async () => await inbox.FetchAsync (uids, 31337, MessageSummaryItems.All, emptyFields)); - - Assert.Throws (() => inbox.Fetch ((IList) null, 31337, MessageSummaryItems.All, fields)); - Assert.Throws (async () => await inbox.FetchAsync ((IList) null, 31337, MessageSummaryItems.All, fields)); - //Assert.Throws (() => inbox.Fetch (indexes, 31337, MessageSummaryItems.None, fields)); - //Assert.Throws (async () => await inbox.FetchAsync (indexes, 31337, MessageSummaryItems.None, fields)); - Assert.Throws (() => inbox.Fetch (indexes, 31337, MessageSummaryItems.All, (HashSet) null)); - Assert.Throws (async () => await inbox.FetchAsync (indexes, 31337, MessageSummaryItems.All, (HashSet) null)); - Assert.Throws (() => inbox.Fetch (indexes, 31337, MessageSummaryItems.All, emptyFields)); - Assert.Throws (async () => await inbox.FetchAsync (indexes, 31337, MessageSummaryItems.All, emptyFields)); - - // GetHeaders - Assert.Throws (() => inbox.GetHeaders (-1)); - Assert.Throws (async () => await inbox.GetHeadersAsync (-1)); - Assert.Throws (() => inbox.GetHeaders (UniqueId.Invalid)); - Assert.Throws (async () => await inbox.GetHeadersAsync (UniqueId.Invalid)); - - var bodyPart = new BodyPartText (); - - Assert.Throws (() => inbox.GetHeaders (-1, bodyPart)); - Assert.Throws (async () => await inbox.GetHeadersAsync (-1, bodyPart)); - Assert.Throws (() => inbox.GetHeaders (0, (BodyPart) null)); - Assert.Throws (async () => await inbox.GetHeadersAsync (0, (BodyPart) null)); - - Assert.Throws (() => inbox.GetHeaders (UniqueId.Invalid, bodyPart)); - Assert.Throws (async () => await inbox.GetHeadersAsync (UniqueId.Invalid, bodyPart)); - Assert.Throws (() => inbox.GetHeaders (UniqueId.MinValue, (BodyPart) null)); - Assert.Throws (async () => await inbox.GetHeadersAsync (UniqueId.MinValue, (BodyPart) null)); - - Assert.Throws (() => inbox.GetHeaders (-1, "1.2")); - //Assert.Throws (async () => await inbox.GetHeadersAsync (-1, "1.2")); - Assert.Throws (() => inbox.GetHeaders (0, (string) null)); - //Assert.Throws (async () => await inbox.GetHeadersAsync (0, (string) null)); - - Assert.Throws (() => inbox.GetHeaders (UniqueId.Invalid, "1.2")); - //Assert.Throws (async () => await inbox.GetHeadersAsync (UniqueId.Invalid, "1.2")); - Assert.Throws (() => inbox.GetHeaders (UniqueId.MinValue, (string) null)); - //Assert.Throws (async () => await inbox.GetHeadersAsync (UniqueId.MinValue, (string) null)); - - // GetMessage - Assert.Throws (() => inbox.GetMessage (-1)); - Assert.Throws (async () => await inbox.GetMessageAsync (-1)); - Assert.Throws (() => inbox.GetMessage (UniqueId.Invalid)); - Assert.Throws (async () => await inbox.GetMessageAsync (UniqueId.Invalid)); - - // GetBodyPart - Assert.Throws (() => inbox.GetBodyPart (-1, bodyPart)); - Assert.Throws (async () => await inbox.GetBodyPartAsync (-1, bodyPart)); - Assert.Throws (() => inbox.GetBodyPart (0, (BodyPart) null)); - Assert.Throws (async () => await inbox.GetBodyPartAsync (0, (BodyPart) null)); - - Assert.Throws (() => inbox.GetBodyPart (UniqueId.Invalid, bodyPart)); - Assert.Throws (async () => await inbox.GetBodyPartAsync (UniqueId.Invalid, bodyPart)); - Assert.Throws (() => inbox.GetBodyPart (UniqueId.MinValue, (BodyPart) null)); - Assert.Throws (async () => await inbox.GetBodyPartAsync (UniqueId.MinValue, (BodyPart) null)); - - Assert.Throws (() => inbox.GetBodyPart (-1, "1.2")); - //Assert.Throws (async () => await inbox.GetBodyPartAsync (-1, "1.2")); - Assert.Throws (() => inbox.GetBodyPart (0, (string) null)); - //Assert.Throws (async () => await inbox.GetBodyPartAsync (0, (string) null)); - - Assert.Throws (() => inbox.GetBodyPart (UniqueId.Invalid, "1.2")); - //Assert.Throws (async () => await inbox.GetBodyPartAsync (UniqueId.Invalid, "1.2")); - Assert.Throws (() => inbox.GetBodyPart (UniqueId.MinValue, (string) null)); - //Assert.Throws (async () => await inbox.GetBodyPartAsync (UniqueId.MinValue, (string) null)); - - Assert.Throws (() => inbox.GetBodyPart (-1, bodyPart, true)); - Assert.Throws (async () => await inbox.GetBodyPartAsync (-1, bodyPart, true)); - Assert.Throws (() => inbox.GetBodyPart (0, (BodyPart) null, true)); - Assert.Throws (async () => await inbox.GetBodyPartAsync (0, (BodyPart) null, true)); - - Assert.Throws (() => inbox.GetBodyPart (UniqueId.Invalid, bodyPart, true)); - Assert.Throws (async () => await inbox.GetBodyPartAsync (UniqueId.Invalid, bodyPart, true)); - Assert.Throws (() => inbox.GetBodyPart (UniqueId.MinValue, (BodyPart) null, true)); - Assert.Throws (async () => await inbox.GetBodyPartAsync (UniqueId.MinValue, (BodyPart) null, true)); - - Assert.Throws (() => inbox.GetBodyPart (-1, "1.2", true)); - //Assert.Throws (async () => await inbox.GetBodyPartAsync (-1, "1.2", true)); - Assert.Throws (() => inbox.GetBodyPart (0, (string) null, true)); - //Assert.Throws (async () => await inbox.GetBodyPartAsync (0, (string) null, true)); - - Assert.Throws (() => inbox.GetBodyPart (UniqueId.Invalid, "1.2", true)); - //Assert.Throws (async () => await inbox.GetBodyPartAsync (UniqueId.Invalid, "1.2", true)); - Assert.Throws (() => inbox.GetBodyPart (UniqueId.MinValue, (string) null, true)); - //Assert.Throws (async () => await inbox.GetBodyPartAsync (UniqueId.MinValue, (string) null, true)); - - // GetStream - Assert.Throws (() => inbox.GetStream (-1, "1.2")); - Assert.Throws (async () => await inbox.GetStreamAsync (-1, "1.2")); - Assert.Throws (() => inbox.GetStream (0, (string) null)); - Assert.Throws (async () => await inbox.GetStreamAsync (0, (string) null)); - - Assert.Throws (() => inbox.GetStream (UniqueId.Invalid, "1.2")); - Assert.Throws (async () => await inbox.GetStreamAsync (UniqueId.Invalid, "1.2")); - Assert.Throws (() => inbox.GetStream (UniqueId.MinValue, (string) null)); - Assert.Throws (async () => await inbox.GetStreamAsync (UniqueId.MinValue, (string) null)); - - //Assert.Throws (() => inbox.GetStream (-1, bodyPart)); - //Assert.Throws (async () => await inbox.GetStreamAsync (-1, bodyPart)); - //Assert.Throws (() => inbox.GetStream (0, (BodyPart) null)); - //Assert.Throws (async () => await inbox.GetStreamAsync (0, (BodyPart) null)); - - //Assert.Throws (() => inbox.GetStream (UniqueId.Invalid, bodyPart)); - //Assert.Throws (async () => await inbox.GetStreamAsync (UniqueId.Invalid, bodyPart)); - //Assert.Throws (() => inbox.GetStream (UniqueId.MinValue, (BodyPart) null)); - //Assert.Throws (async () => await inbox.GetStreamAsync (UniqueId.MinValue, (BodyPart) null)); - - Assert.Throws (() => inbox.GetStream (-1, 0, 1024)); - Assert.Throws (async () => await inbox.GetStreamAsync (-1, 0, 1024)); - Assert.Throws (() => inbox.GetStream (0, -1, 1024)); - Assert.Throws (async () => await inbox.GetStreamAsync (0, -1, 1024)); - Assert.Throws (() => inbox.GetStream (0, 0, -1)); - Assert.Throws (async () => await inbox.GetStreamAsync (0, 0, -1)); - - Assert.Throws (() => inbox.GetStream (UniqueId.Invalid, 0, 1024)); - Assert.Throws (async () => await inbox.GetStreamAsync (UniqueId.Invalid, 0, 1024)); - Assert.Throws (() => inbox.GetStream (UniqueId.MinValue, -1, 1024)); - Assert.Throws (async () => await inbox.GetStreamAsync (UniqueId.MinValue, -1, 1024)); - Assert.Throws (() => inbox.GetStream (UniqueId.MinValue, 0, -1)); - Assert.Throws (async () => await inbox.GetStreamAsync (UniqueId.MinValue, 0, -1)); - - Assert.Throws (() => inbox.GetStream (-1, "1.2", 0, 1024)); - Assert.Throws (async () => await inbox.GetStreamAsync (-1, "1.2", 0, 1024)); - Assert.Throws (() => inbox.GetStream (0, (string) null, 0, 1024)); - Assert.Throws (async () => await inbox.GetStreamAsync (0, (string) null, 0, 1024)); - Assert.Throws (() => inbox.GetStream (0, "1.2", -1, 1024)); - Assert.Throws (async () => await inbox.GetStreamAsync (0, "1.2", -1, 1024)); - Assert.Throws (() => inbox.GetStream (0, "1.2", 0, -1)); - Assert.Throws (async () => await inbox.GetStreamAsync (0, "1.2", 0, -1)); - - Assert.Throws (() => inbox.GetStream (UniqueId.Invalid, "1.2", 0, 1024)); - Assert.Throws (async () => await inbox.GetStreamAsync (UniqueId.Invalid, "1.2", 0, 1024)); - Assert.Throws (() => inbox.GetStream (UniqueId.MinValue, (string) null, 0, 1024)); - Assert.Throws (async () => await inbox.GetStreamAsync (UniqueId.MinValue, (string) null, 0, 1024)); - Assert.Throws (() => inbox.GetStream (UniqueId.MinValue, "1.2", -1, 1024)); - Assert.Throws (async () => await inbox.GetStreamAsync (UniqueId.MinValue, "1.2", -1, 1024)); - Assert.Throws (() => inbox.GetStream (UniqueId.MinValue, "1.2", 0, -1)); - Assert.Throws (async () => await inbox.GetStreamAsync (UniqueId.MinValue, "1.2", 0, -1)); - - Assert.Throws (() => inbox.GetStream (-1, bodyPart, 0, 1024)); - Assert.Throws (async () => await inbox.GetStreamAsync (-1, bodyPart, 0, 1024)); - Assert.Throws (() => inbox.GetStream (0, (BodyPart) null, -1, 1024)); - Assert.Throws (async () => await inbox.GetStreamAsync (0, (BodyPart) null, -1, 1024)); - Assert.Throws (() => inbox.GetStream (0, bodyPart, -1, 1024)); - Assert.Throws (async () => await inbox.GetStreamAsync (0, bodyPart, -1, 1024)); - Assert.Throws (() => inbox.GetStream (0, bodyPart, 0, -1)); - Assert.Throws (async () => await inbox.GetStreamAsync (0, bodyPart, 0, -1)); - - Assert.Throws (() => inbox.GetStream (UniqueId.Invalid, bodyPart, 0, 1024)); - Assert.Throws (async () => await inbox.GetStreamAsync (UniqueId.Invalid, bodyPart, 0, 1024)); - Assert.Throws (() => inbox.GetStream (UniqueId.MinValue, (BodyPart) null, -1, 1024)); - Assert.Throws (async () => await inbox.GetStreamAsync (UniqueId.MinValue, (BodyPart) null, -1, 1024)); - Assert.Throws (() => inbox.GetStream (UniqueId.MinValue, bodyPart, -1, 1024)); - Assert.Throws (async () => await inbox.GetStreamAsync (UniqueId.MinValue, bodyPart, -1, 1024)); - Assert.Throws (() => inbox.GetStream (UniqueId.MinValue, bodyPart, 0, -1)); - Assert.Throws (async () => await inbox.GetStreamAsync (UniqueId.MinValue, bodyPart, 0, -1)); - - // AddFlags - Assert.Throws (() => inbox.AddFlags (-1, MessageFlags.Seen, true)); - Assert.Throws (async () => await inbox.AddFlagsAsync (-1, MessageFlags.Seen, true)); - Assert.Throws (() => inbox.AddFlags (0, MessageFlags.None, true)); - Assert.Throws (async () => await inbox.AddFlagsAsync (0, MessageFlags.None, true)); - Assert.Throws (() => inbox.AddFlags (UniqueId.MinValue, MessageFlags.None, true)); - Assert.Throws (async () => await inbox.AddFlagsAsync (UniqueId.MinValue, MessageFlags.None, true)); - Assert.Throws (() => inbox.AddFlags ((IList) null, MessageFlags.Seen, true)); - Assert.Throws (async () => await inbox.AddFlagsAsync ((IList) null, MessageFlags.Seen, true)); - Assert.Throws (() => inbox.AddFlags ((IList) null, MessageFlags.Seen, true)); - Assert.Throws (async () => await inbox.AddFlagsAsync ((IList) null, MessageFlags.Seen, true)); - Assert.Throws (() => inbox.AddFlags (new int[] { 0 }, MessageFlags.None, true)); - Assert.Throws (async () => await inbox.AddFlagsAsync (new int[] { 0 }, MessageFlags.None, true)); - Assert.Throws (() => inbox.AddFlags (UniqueIdRange.All, MessageFlags.None, true)); - Assert.Throws (async () => await inbox.AddFlagsAsync (UniqueIdRange.All, MessageFlags.None, true)); - Assert.Throws (() => inbox.AddFlags ((IList) null, 1, MessageFlags.Seen, true)); - Assert.Throws (async () => await inbox.AddFlagsAsync ((IList) null, 1, MessageFlags.Seen, true)); - Assert.Throws (() => inbox.AddFlags ((IList) null, 1, MessageFlags.Seen, true)); - Assert.Throws (async () => await inbox.AddFlagsAsync ((IList) null, 1, MessageFlags.Seen, true)); - Assert.Throws (() => inbox.AddFlags (new int[] { 0 }, 1, MessageFlags.None, true)); - Assert.Throws (async () => await inbox.AddFlagsAsync (new int[] { 0 }, 1, MessageFlags.None, true)); - Assert.Throws (() => inbox.AddFlags (UniqueIdRange.All, 1, MessageFlags.None, true)); - Assert.Throws (async () => await inbox.AddFlagsAsync (UniqueIdRange.All, 1, MessageFlags.None, true)); - - // RemoveFlags - Assert.Throws (() => inbox.RemoveFlags (-1, MessageFlags.Seen, true)); - Assert.Throws (async () => await inbox.RemoveFlagsAsync (-1, MessageFlags.Seen, true)); - Assert.Throws (() => inbox.RemoveFlags (0, MessageFlags.None, true)); - Assert.Throws (async () => await inbox.RemoveFlagsAsync (0, MessageFlags.None, true)); - Assert.Throws (() => inbox.RemoveFlags (UniqueId.MinValue, MessageFlags.None, true)); - Assert.Throws (async () => await inbox.RemoveFlagsAsync (UniqueId.MinValue, MessageFlags.None, true)); - Assert.Throws (() => inbox.RemoveFlags ((IList) null, MessageFlags.Seen, true)); - Assert.Throws (async () => await inbox.RemoveFlagsAsync ((IList) null, MessageFlags.Seen, true)); - Assert.Throws (() => inbox.RemoveFlags ((IList) null, MessageFlags.Seen, true)); - Assert.Throws (async () => await inbox.RemoveFlagsAsync ((IList) null, MessageFlags.Seen, true)); - Assert.Throws (() => inbox.RemoveFlags (new int[] { 0 }, MessageFlags.None, true)); - Assert.Throws (async () => await inbox.RemoveFlagsAsync (new int[] { 0 }, MessageFlags.None, true)); - Assert.Throws (() => inbox.RemoveFlags (UniqueIdRange.All, MessageFlags.None, true)); - Assert.Throws (async () => await inbox.RemoveFlagsAsync (UniqueIdRange.All, MessageFlags.None, true)); - Assert.Throws (() => inbox.RemoveFlags ((IList) null, 1, MessageFlags.Seen, true)); - Assert.Throws (async () => await inbox.RemoveFlagsAsync ((IList) null, 1, MessageFlags.Seen, true)); - Assert.Throws (() => inbox.RemoveFlags ((IList) null, 1, MessageFlags.Seen, true)); - Assert.Throws (async () => await inbox.RemoveFlagsAsync ((IList) null, 1, MessageFlags.Seen, true)); - Assert.Throws (() => inbox.RemoveFlags (new int[] { 0 }, 1, MessageFlags.None, true)); - Assert.Throws (async () => await inbox.RemoveFlagsAsync (new int[] { 0 }, 1, MessageFlags.None, true)); - Assert.Throws (() => inbox.RemoveFlags (UniqueIdRange.All, 1, MessageFlags.None, true)); - Assert.Throws (async () => await inbox.RemoveFlagsAsync (UniqueIdRange.All, 1, MessageFlags.None, true)); - - // SetFlags - Assert.Throws (() => inbox.SetFlags (-1, MessageFlags.Seen, true)); - Assert.Throws (async () => await inbox.SetFlagsAsync (-1, MessageFlags.Seen, true)); - Assert.Throws (() => inbox.SetFlags ((IList) null, MessageFlags.Seen, true)); - Assert.Throws (async () => await inbox.SetFlagsAsync ((IList) null, MessageFlags.Seen, true)); - Assert.Throws (() => inbox.SetFlags ((IList) null, MessageFlags.Seen, true)); - Assert.Throws (async () => await inbox.SetFlagsAsync ((IList) null, MessageFlags.Seen, true)); - Assert.Throws (() => inbox.SetFlags ((IList) null, 1, MessageFlags.Seen, true)); - Assert.Throws (async () => await inbox.SetFlagsAsync ((IList) null, 1, MessageFlags.Seen, true)); - Assert.Throws (() => inbox.SetFlags ((IList) null, 1, MessageFlags.Seen, true)); - Assert.Throws (async () => await inbox.SetFlagsAsync ((IList) null, 1, MessageFlags.Seen, true)); + Assert.That (client.PersonalNamespaces, Has.Count.EqualTo (1), "Personal"); + Assert.That (client.SharedNamespaces, Is.Empty, "Shared"); + Assert.That (client.OtherNamespaces, Is.Empty, "Other"); - var labels = new string[] { "Label1", "Label2" }; - var emptyLabels = new string[0]; - - // AddLabels - Assert.Throws (() => inbox.AddLabels (-1, labels, true)); - Assert.Throws (async () => await inbox.AddLabelsAsync (-1, labels, true)); - Assert.Throws (() => inbox.AddLabels (0, null, true)); - Assert.Throws (async () => await inbox.AddLabelsAsync (0, null, true)); - Assert.Throws (() => inbox.AddLabels (UniqueId.MinValue, null, true)); - Assert.Throws (async () => await inbox.AddLabelsAsync (UniqueId.MinValue, null, true)); - Assert.Throws (() => inbox.AddLabels ((IList) null, labels, true)); - Assert.Throws (async () => await inbox.AddLabelsAsync ((IList) null, labels, true)); - Assert.Throws (() => inbox.AddLabels ((IList) null, labels, true)); - Assert.Throws (async () => await inbox.AddLabelsAsync ((IList) null, labels, true)); - Assert.Throws (() => inbox.AddLabels (new int[] { 0 }, null, true)); - Assert.Throws (async () => await inbox.AddLabelsAsync (new int[] { 0 }, null, true)); - Assert.Throws (() => inbox.AddLabels (UniqueIdRange.All, null, true)); - Assert.Throws (async () => await inbox.AddLabelsAsync (UniqueIdRange.All, null, true)); - Assert.Throws (() => inbox.AddLabels (new int[] { 0 }, emptyLabels, true)); - Assert.Throws (async () => await inbox.AddLabelsAsync (new int[] { 0 }, emptyLabels, true)); - Assert.Throws (() => inbox.AddLabels (UniqueIdRange.All, emptyLabels, true)); - Assert.Throws (async () => await inbox.AddLabelsAsync (UniqueIdRange.All, emptyLabels, true)); - - Assert.Throws (() => inbox.AddLabels ((IList) null, 1, labels, true)); - Assert.Throws (async () => await inbox.AddLabelsAsync ((IList) null, 1, labels, true)); - Assert.Throws (() => inbox.AddLabels ((IList) null, 1, labels, true)); - Assert.Throws (async () => await inbox.AddLabelsAsync ((IList) null, 1, labels, true)); - Assert.Throws (() => inbox.AddLabels (new int[] { 0 }, 1, null, true)); - Assert.Throws (async () => await inbox.AddLabelsAsync (new int[] { 0 }, 1, null, true)); - Assert.Throws (() => inbox.AddLabels (UniqueIdRange.All, 1, null, true)); - Assert.Throws (async () => await inbox.AddLabelsAsync (UniqueIdRange.All, 1, null, true)); - Assert.Throws (() => inbox.AddLabels (new int[] { 0 }, 1, emptyLabels, true)); - Assert.Throws (async () => await inbox.AddLabelsAsync (new int[] { 0 }, 1, emptyLabels, true)); - Assert.Throws (() => inbox.AddLabels (UniqueIdRange.All, 1, emptyLabels, true)); - Assert.Throws (async () => await inbox.AddLabelsAsync (UniqueIdRange.All, 1, emptyLabels, true)); - - // RemoveLabels - Assert.Throws (() => inbox.RemoveLabels (-1, labels, true)); - Assert.Throws (async () => await inbox.RemoveLabelsAsync (-1, labels, true)); - Assert.Throws (() => inbox.RemoveLabels (0, null, true)); - Assert.Throws (async () => await inbox.RemoveLabelsAsync (0, null, true)); - Assert.Throws (() => inbox.RemoveLabels (UniqueId.MinValue, null, true)); - Assert.Throws (async () => await inbox.RemoveLabelsAsync (UniqueId.MinValue, null, true)); - Assert.Throws (() => inbox.RemoveLabels ((IList) null, labels, true)); - Assert.Throws (async () => await inbox.RemoveLabelsAsync ((IList) null, labels, true)); - Assert.Throws (() => inbox.RemoveLabels ((IList) null, labels, true)); - Assert.Throws (async () => await inbox.RemoveLabelsAsync ((IList) null, labels, true)); - Assert.Throws (() => inbox.RemoveLabels (new int[] { 0 }, null, true)); - Assert.Throws (async () => await inbox.RemoveLabelsAsync (new int[] { 0 }, null, true)); - Assert.Throws (() => inbox.RemoveLabels (UniqueIdRange.All, null, true)); - Assert.Throws (async () => await inbox.RemoveLabelsAsync (UniqueIdRange.All, null, true)); - Assert.Throws (() => inbox.RemoveLabels (new int[] { 0 }, emptyLabels, true)); - Assert.Throws (async () => await inbox.RemoveLabelsAsync (new int[] { 0 }, emptyLabels, true)); - Assert.Throws (() => inbox.RemoveLabels (UniqueIdRange.All, emptyLabels, true)); - Assert.Throws (async () => await inbox.RemoveLabelsAsync (UniqueIdRange.All, emptyLabels, true)); - - Assert.Throws (() => inbox.RemoveLabels ((IList) null, 1, labels, true)); - Assert.Throws (async () => await inbox.RemoveLabelsAsync ((IList) null, 1, labels, true)); - Assert.Throws (() => inbox.RemoveLabels ((IList) null, 1, labels, true)); - Assert.Throws (async () => await inbox.RemoveLabelsAsync ((IList) null, 1, labels, true)); - Assert.Throws (() => inbox.RemoveLabels (new int[] { 0 }, 1, null, true)); - Assert.Throws (async () => await inbox.RemoveLabelsAsync (new int[] { 0 }, 1, null, true)); - Assert.Throws (() => inbox.RemoveLabels (UniqueIdRange.All, 1, null, true)); - Assert.Throws (async () => await inbox.RemoveLabelsAsync (UniqueIdRange.All, 1, null, true)); - Assert.Throws (() => inbox.RemoveLabels (new int[] { 0 }, 1, emptyLabels, true)); - Assert.Throws (async () => await inbox.RemoveLabelsAsync (new int[] { 0 }, 1, emptyLabels, true)); - Assert.Throws (() => inbox.RemoveLabels (UniqueIdRange.All, 1, emptyLabels, true)); - Assert.Throws (async () => await inbox.RemoveLabelsAsync (UniqueIdRange.All, 1, emptyLabels, true)); - - // SetLabels - Assert.Throws (() => inbox.SetLabels (-1, labels, true)); - Assert.Throws (async () => await inbox.SetLabelsAsync (-1, labels, true)); - Assert.Throws (() => inbox.SetLabels (0, null, true)); - Assert.Throws (async () => await inbox.SetLabelsAsync (0, null, true)); - Assert.Throws (() => inbox.SetLabels (UniqueId.MinValue, null, true)); - Assert.Throws (async () => await inbox.SetLabelsAsync (UniqueId.MinValue, null, true)); - Assert.Throws (() => inbox.SetLabels ((IList) null, labels, true)); - Assert.Throws (async () => await inbox.SetLabelsAsync ((IList) null, labels, true)); - Assert.Throws (() => inbox.SetLabels ((IList) null, labels, true)); - Assert.Throws (async () => await inbox.SetLabelsAsync ((IList) null, labels, true)); - Assert.Throws (() => inbox.SetLabels (new int[] { 0 }, null, true)); - Assert.Throws (async () => await inbox.SetLabelsAsync (new int[] { 0 }, null, true)); - Assert.Throws (() => inbox.SetLabels (UniqueIdRange.All, null, true)); - Assert.Throws (async () => await inbox.SetLabelsAsync (UniqueIdRange.All, null, true)); - - Assert.Throws (() => inbox.SetLabels ((IList) null, 1, labels, true)); - Assert.Throws (async () => await inbox.SetLabelsAsync ((IList) null, 1, labels, true)); - Assert.Throws (() => inbox.SetLabels ((IList) null, 1, labels, true)); - Assert.Throws (async () => await inbox.SetLabelsAsync ((IList) null, 1, labels, true)); - Assert.Throws (() => inbox.SetLabels (new int[] { 0 }, 1, null, true)); - Assert.Throws (async () => await inbox.SetLabelsAsync (new int[] { 0 }, 1, null, true)); - Assert.Throws (() => inbox.SetLabels (UniqueIdRange.All, 1, null, true)); - Assert.Throws (async () => await inbox.SetLabelsAsync (UniqueIdRange.All, 1, null, true)); - - // Search - var searchOptions = SearchOptions.All | SearchOptions.Min | SearchOptions.Max | SearchOptions.Count; - var orderBy = new OrderBy[] { OrderBy.Arrival }; - var emptyOrderBy = new OrderBy[0]; - - Assert.Throws (() => inbox.Search ((SearchQuery) null)); - Assert.Throws (async () => await inbox.SearchAsync ((SearchQuery) null)); - Assert.Throws (() => inbox.Search ((SearchQuery) null, orderBy)); - Assert.Throws (async () => await inbox.SearchAsync ((SearchQuery) null, orderBy)); - Assert.Throws (() => inbox.Search (SearchQuery.All, null)); - Assert.Throws (async () => await inbox.SearchAsync (SearchQuery.All, null)); - Assert.Throws (() => inbox.Search (SearchQuery.All, emptyOrderBy)); - Assert.Throws (async () => await inbox.SearchAsync (SearchQuery.All, emptyOrderBy)); - Assert.Throws (() => inbox.Search ((IList) null, SearchQuery.All)); - Assert.Throws (async () => await inbox.SearchAsync ((IList) null, SearchQuery.All)); - Assert.Throws (() => inbox.Search (UniqueIdRange.All, (SearchQuery) null)); - Assert.Throws (async () => await inbox.SearchAsync (UniqueIdRange.All, (SearchQuery) null)); - Assert.Throws (() => inbox.Search ((IList) null, SearchQuery.All, orderBy)); - Assert.Throws (async () => await inbox.SearchAsync ((IList) null, SearchQuery.All, orderBy)); - Assert.Throws (() => inbox.Search (UniqueIdRange.All, (SearchQuery) null, orderBy)); - Assert.Throws (async () => await inbox.SearchAsync (UniqueIdRange.All, (SearchQuery) null, orderBy)); - Assert.Throws (() => inbox.Search (UniqueIdRange.All, SearchQuery.All, null)); - Assert.Throws (async () => await inbox.SearchAsync (UniqueIdRange.All, SearchQuery.All, null)); - Assert.Throws (() => inbox.Search (UniqueIdRange.All, SearchQuery.All, emptyOrderBy)); - Assert.Throws (async () => await inbox.SearchAsync (UniqueIdRange.All, SearchQuery.All, emptyOrderBy)); - Assert.Throws (() => inbox.Search (searchOptions, null)); - Assert.Throws (async () => await inbox.SearchAsync (searchOptions, null)); - Assert.Throws (() => inbox.Search (searchOptions, (SearchQuery) null, orderBy)); - Assert.Throws (async () => await inbox.SearchAsync (searchOptions, (SearchQuery) null, orderBy)); - Assert.Throws (() => inbox.Search (searchOptions, SearchQuery.All, null)); - Assert.Throws (async () => await inbox.SearchAsync (searchOptions, SearchQuery.All, null)); - Assert.Throws (() => inbox.Search (searchOptions, SearchQuery.All, emptyOrderBy)); - Assert.Throws (async () => await inbox.SearchAsync (searchOptions, SearchQuery.All, emptyOrderBy)); - Assert.Throws (() => inbox.Search (searchOptions, (IList) null, SearchQuery.All)); - Assert.Throws (async () => await inbox.SearchAsync (searchOptions, (IList) null, SearchQuery.All)); - Assert.Throws (() => inbox.Search (searchOptions, UniqueIdRange.All, (SearchQuery) null)); - Assert.Throws (async () => await inbox.SearchAsync (searchOptions, UniqueIdRange.All, (SearchQuery) null)); - Assert.Throws (() => inbox.Search (searchOptions, (IList) null, SearchQuery.All, orderBy)); - Assert.Throws (async () => await inbox.SearchAsync (searchOptions, (IList) null, SearchQuery.All, orderBy)); - Assert.Throws (() => inbox.Search (searchOptions, UniqueIdRange.All, (SearchQuery) null, orderBy)); - Assert.Throws (async () => await inbox.SearchAsync (searchOptions, UniqueIdRange.All, (SearchQuery) null, orderBy)); - Assert.Throws (() => inbox.Search (searchOptions, UniqueIdRange.All, SearchQuery.All, null)); - Assert.Throws (async () => await inbox.SearchAsync (searchOptions, UniqueIdRange.All, SearchQuery.All, null)); - Assert.Throws (() => inbox.Search (searchOptions, UniqueIdRange.All, SearchQuery.All, emptyOrderBy)); - Assert.Throws (async () => await inbox.SearchAsync (searchOptions, UniqueIdRange.All, SearchQuery.All, emptyOrderBy)); - - Assert.Throws (() => inbox.Search ((string) null)); - Assert.Throws (async () => await inbox.SearchAsync ((string) null)); - - // Sort - Assert.Throws (() => inbox.Sort ((SearchQuery) null, orderBy)); - Assert.Throws (async () => await inbox.SortAsync ((SearchQuery) null, orderBy)); - Assert.Throws (() => inbox.Sort (SearchQuery.All, null)); - Assert.Throws (async () => await inbox.SortAsync (SearchQuery.All, null)); - Assert.Throws (() => inbox.Sort (SearchQuery.All, emptyOrderBy)); - Assert.Throws (async () => await inbox.SortAsync (SearchQuery.All, emptyOrderBy)); - - Assert.Throws (() => inbox.Sort ((IList) null, SearchQuery.All, orderBy)); - Assert.Throws (async () => await inbox.SortAsync ((IList) null, SearchQuery.All, orderBy)); - Assert.Throws (() => inbox.Sort (UniqueIdRange.All, (SearchQuery) null, orderBy)); - Assert.Throws (async () => await inbox.SortAsync (UniqueIdRange.All, (SearchQuery) null, orderBy)); - Assert.Throws (() => inbox.Sort (UniqueIdRange.All, SearchQuery.All, null)); - Assert.Throws (async () => await inbox.SortAsync (UniqueIdRange.All, SearchQuery.All, null)); - Assert.Throws (() => inbox.Sort (UniqueIdRange.All, SearchQuery.All, emptyOrderBy)); - Assert.Throws (async () => await inbox.SortAsync (UniqueIdRange.All, SearchQuery.All, emptyOrderBy)); - - Assert.Throws (() => inbox.Sort (searchOptions, (SearchQuery) null, orderBy)); - Assert.Throws (async () => await inbox.SortAsync (searchOptions, (SearchQuery) null, orderBy)); - Assert.Throws (() => inbox.Sort (searchOptions, SearchQuery.All, null)); - Assert.Throws (async () => await inbox.SortAsync (searchOptions, SearchQuery.All, null)); - Assert.Throws (() => inbox.Sort (searchOptions, SearchQuery.All, emptyOrderBy)); - Assert.Throws (async () => await inbox.SortAsync (searchOptions, SearchQuery.All, emptyOrderBy)); - - Assert.Throws (() => inbox.Sort (searchOptions, (IList) null, SearchQuery.All, orderBy)); - Assert.Throws (async () => await inbox.SortAsync (searchOptions, (IList) null, SearchQuery.All, orderBy)); - Assert.Throws (() => inbox.Sort (searchOptions, UniqueIdRange.All, (SearchQuery) null, orderBy)); - Assert.Throws (async () => await inbox.SortAsync (searchOptions, UniqueIdRange.All, (SearchQuery) null, orderBy)); - Assert.Throws (() => inbox.Sort (searchOptions, UniqueIdRange.All, SearchQuery.All, null)); - Assert.Throws (async () => await inbox.SortAsync (searchOptions, UniqueIdRange.All, SearchQuery.All, null)); - Assert.Throws (() => inbox.Sort (searchOptions, UniqueIdRange.All, SearchQuery.All, emptyOrderBy)); - Assert.Throws (async () => await inbox.SortAsync (searchOptions, UniqueIdRange.All, SearchQuery.All, emptyOrderBy)); - - Assert.Throws (() => inbox.Sort ((string) null)); - Assert.Throws (async () => await inbox.SortAsync ((string) null)); - - // Thread - Assert.Throws (() => inbox.Thread ((ThreadingAlgorithm) 500, SearchQuery.All)); - Assert.Throws (async () => await inbox.ThreadAsync ((ThreadingAlgorithm) 500, SearchQuery.All)); - Assert.Throws (() => inbox.Thread (ThreadingAlgorithm.References, null)); - Assert.Throws (async () => await inbox.ThreadAsync (ThreadingAlgorithm.References, null)); - Assert.Throws (() => inbox.Thread ((IList) null, ThreadingAlgorithm.References, SearchQuery.All)); - Assert.Throws (async () => await inbox.ThreadAsync ((IList) null, ThreadingAlgorithm.References, SearchQuery.All)); - Assert.Throws (() => inbox.Thread (UniqueIdRange.All, (ThreadingAlgorithm) 500, SearchQuery.All)); - Assert.Throws (async () => await inbox.ThreadAsync (UniqueIdRange.All, (ThreadingAlgorithm) 500, SearchQuery.All)); - Assert.Throws (() => inbox.Thread (UniqueIdRange.All, ThreadingAlgorithm.References, null)); - Assert.Throws (async () => await inbox.ThreadAsync (UniqueIdRange.All, ThreadingAlgorithm.References, null)); + var personal = client.GetFolder (client.PersonalNamespaces[0]); client.Disconnect (false); } } + static IList CreateIMAP4rev2Commands () + { + return new List { + new ImapReplayCommand ("", Encoding.ASCII.GetBytes ("* OK [CAPABILITY STARTTLS AUTH=SCRAM-SHA-256 LOGINDISABLED IMAP4rev2] IMAP4rev2 Service Ready\r\n")), + }; + } + [Test] - public void TestImapClientGreetingCapabilities () + public void TestIMAP4rev2 () { - var commands = new List (); - commands.Add (new ImapReplayCommand ("", "common.capability-greeting.txt")); + var commands = CreateIMAP4rev2Commands (); - using (var client = new ImapClient ()) { + using (var client = new ImapClient () { TagPrefix = 'A' }) { try { - client.ReplayConnect ("localhost", new ImapReplayStream (commands, false)); + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); } catch (Exception ex) { - Assert.Fail ("Did not expect an exception in Connect: {0}", ex); + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); } - Assert.IsTrue (client.IsConnected, "Client failed to connect."); - - Assert.AreEqual (GreetingCapabilities, client.Capabilities); - Assert.AreEqual (1, client.AuthenticationMechanisms.Count); - Assert.IsTrue (client.AuthenticationMechanisms.Contains ("PLAIN"), "Expected SASL PLAIN auth mechanism"); + Assert.That (client.Capabilities, Is.EqualTo (IMAP4rev2CoreCapabilities | ImapCapabilities.StartTLS | ImapCapabilities.LoginDisabled), "Capabilities"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("SCRAM-SHA-256"), "AUTH=SCRAM-SHA-256"); } } [Test] - public async void TestImapClientFeatures () + public async Task TestIMAP4rev2Async () { - var commands = new List (); - commands.Add (new ImapReplayCommand ("", "gmail.greeting.txt")); - commands.Add (new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt")); - commands.Add (new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "gmail.authenticate.txt")); - commands.Add (new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt")); - commands.Add (new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\"\r\n", "gmail.list-inbox.txt")); - commands.Add (new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt")); - commands.Add (new ImapReplayCommand ("A00000005 ID (\"name\" \"MailKit\" \"version\" \"1.0\" \"vendor\" \"Xamarin Inc.\")\r\n", "common.id.txt")); - commands.Add (new ImapReplayCommand ("A00000006 GETQUOTAROOT INBOX\r\n", "common.getquota.txt")); - commands.Add (new ImapReplayCommand ("A00000007 SETQUOTA \"\" (MESSAGE 1000000 STORAGE 5242880)\r\n", "common.setquota.txt")); + var commands = CreateIMAP4rev2Commands (); - using (var client = new ImapClient ()) { + using (var client = new ImapClient () { TagPrefix = 'A' }) { try { - client.ReplayConnect ("localhost", new ImapReplayStream (commands, false)); + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); } catch (Exception ex) { - Assert.Fail ("Did not expect an exception in Connect: {0}", ex); + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); } - Assert.IsTrue (client.IsConnected, "Client failed to connect."); - Assert.IsFalse (client.IsSecure, "IsSecure should be false."); + Assert.That (client.Capabilities, Is.EqualTo (IMAP4rev2CoreCapabilities | ImapCapabilities.StartTLS | ImapCapabilities.LoginDisabled), "Capabilities"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("SCRAM-SHA-256"), "AUTH=SCRAM-SHA-256"); + } + } - Assert.AreEqual (GMailInitialCapabilities, client.Capabilities); - Assert.AreEqual (5, client.AuthenticationMechanisms.Count); - Assert.IsTrue (client.AuthenticationMechanisms.Contains ("XOAUTH"), "Expected SASL XOAUTH auth mechanism"); - Assert.IsTrue (client.AuthenticationMechanisms.Contains ("XOAUTH2"), "Expected SASL XOAUTH2 auth mechanism"); - Assert.IsTrue (client.AuthenticationMechanisms.Contains ("OAUTHBEARER"), "Expected SASL OAUTHBEARER auth mechanism"); - Assert.IsTrue (client.AuthenticationMechanisms.Contains ("PLAIN"), "Expected SASL PLAIN auth mechanism"); - Assert.IsTrue (client.AuthenticationMechanisms.Contains ("PLAIN-CLIENTTOKEN"), "Expected SASL PLAIN-CLIENTTOKEN auth mechanism"); + [Test] + public void TestEscapeUserName () + { + var builder = new StringBuilder (); + ImapClient.EscapeUserName (builder, "user:/?@&=+$%,;name"); + var escaped = builder.ToString (); - Assert.AreEqual (100000, client.Timeout, "Timeout"); - client.Timeout *= 2; + Assert.That (escaped, Is.EqualTo ("user%3A%2F%3F%40%26%3D%2B%24%25%2C%3Bname")); + } - // Note: Do not try XOAUTH2 - client.AuthenticationMechanisms.Remove ("XOAUTH2"); + [Test] + public void TestUnescapeUserName () + { + var unescaped = ImapClient.UnescapeUserName ("user%3A%2F%3F%40%26%3D%2B%24%25%2C%3Bname"); - try { - await client.AuthenticateAsync (new NetworkCredential ("username", "password")); - } catch (Exception ex) { - Assert.Fail ("Did not expect an exception in Authenticate: {0}", ex); - } + Assert.That (unescaped, Is.EqualTo ("user:/?@&=+$%,;name")); - Assert.AreEqual (GMailAuthenticatedCapabilities, client.Capabilities); - Assert.IsTrue (client.SupportsQuotas, "SupportsQuotas"); + unescaped = ImapClient.UnescapeUserName ("user%3a%2f%3f%40%26%3d%2b%24%25%2c%3bname"); - var implementation = new ImapImplementation { - Name = "MailKit", Version = "1.0", Vendor = "Xamarin Inc." - }; + Assert.That (unescaped, Is.EqualTo ("user:/?@&=+$%,;name")); + } - implementation = await client.IdentifyAsync (implementation); - Assert.IsNotNull (implementation, "Expected a non-null ID response."); - Assert.AreEqual ("GImap", implementation.Name); - Assert.AreEqual ("Google, Inc.", implementation.Vendor); - Assert.AreEqual ("http://support.google.com/mail", implementation.SupportUrl); - Assert.AreEqual ("gmail_imap_150623.03_p1", implementation.Version); - Assert.AreEqual ("127.0.0.1", implementation.Properties["remote-host"]); + static void AssertDefaultValues (string host, int port, SecureSocketOptions options, Uri expected) + { + ImapClient.ComputeDefaultValues (host, ref port, ref options, out Uri uri, out bool starttls); + + if (expected.PathAndQuery == "/?starttls=when-available") { + Assert.That (options, Is.EqualTo (SecureSocketOptions.StartTlsWhenAvailable), $"{expected}"); + Assert.That (starttls, Is.True, $"{expected}"); + } else if (expected.PathAndQuery == "/?starttls=always") { + Assert.That (options, Is.EqualTo (SecureSocketOptions.StartTls), $"{expected}"); + Assert.That (starttls, Is.True, $"{expected}"); + } else if (expected.Scheme == "imaps") { + Assert.That (options, Is.EqualTo (SecureSocketOptions.SslOnConnect), $"{expected}"); + Assert.That (starttls, Is.False, $"{expected}"); + } else { + Assert.That (options, Is.EqualTo (SecureSocketOptions.None), $"{expected}"); + Assert.That (starttls, Is.False, $"{expected}"); + } - var personal = client.GetFolder (client.PersonalNamespaces[0]); - var inbox = client.Inbox; + Assert.That (uri.ToString (), Is.EqualTo (expected.ToString ())); + Assert.That (port, Is.EqualTo (expected.Port), $"{expected}"); + } - Assert.IsNotNull (inbox, "Expected non-null Inbox folder."); - Assert.AreEqual (FolderAttributes.Inbox | FolderAttributes.HasNoChildren, inbox.Attributes, "Expected Inbox attributes to be \\HasNoChildren."); + [Test] + public void TestComputeDefaultValues () + { + const string host = "imap.skyfall.net"; - var quota = await inbox.GetQuotaAsync (); - Assert.IsNotNull (quota, "Expected a non-null GETQUOTAROOT response."); - Assert.AreEqual (personal.FullName, quota.QuotaRoot.FullName); - Assert.AreEqual (personal, quota.QuotaRoot); - Assert.AreEqual (3783, quota.CurrentStorageSize.Value); - Assert.AreEqual (15728640, quota.StorageLimit.Value); - Assert.IsFalse (quota.CurrentMessageCount.HasValue); - Assert.IsFalse (quota.MessageLimit.HasValue); + AssertDefaultValues (host, 0, SecureSocketOptions.None, new Uri ($"imap://{host}:143")); + AssertDefaultValues (host, 143, SecureSocketOptions.None, new Uri ($"imap://{host}:143")); + AssertDefaultValues (host, 993, SecureSocketOptions.None, new Uri ($"imap://{host}:993")); - quota = await personal.SetQuotaAsync (1000000, 5242880); - Assert.IsNotNull (quota, "Expected non-null SETQUOTA response."); - Assert.AreEqual (1107, quota.CurrentMessageCount.Value); - Assert.AreEqual (3783, quota.CurrentStorageSize.Value); - Assert.AreEqual (1000000, quota.MessageLimit.Value); - Assert.AreEqual (5242880, quota.StorageLimit.Value); + AssertDefaultValues (host, 0, SecureSocketOptions.SslOnConnect, new Uri ($"imaps://{host}:993")); + AssertDefaultValues (host, 143, SecureSocketOptions.SslOnConnect, new Uri ($"imaps://{host}:143")); + AssertDefaultValues (host, 993, SecureSocketOptions.SslOnConnect, new Uri ($"imaps://{host}:993")); - await client.DisconnectAsync (false); - } + AssertDefaultValues (host, 0, SecureSocketOptions.StartTls, new Uri ($"imap://{host}:143/?starttls=always")); + AssertDefaultValues (host, 143, SecureSocketOptions.StartTls, new Uri ($"imap://{host}:143/?starttls=always")); + AssertDefaultValues (host, 993, SecureSocketOptions.StartTls, new Uri ($"imap://{host}:993/?starttls=always")); + + AssertDefaultValues (host, 0, SecureSocketOptions.StartTlsWhenAvailable, new Uri ($"imap://{host}:143/?starttls=when-available")); + AssertDefaultValues (host, 143, SecureSocketOptions.StartTlsWhenAvailable, new Uri ($"imap://{host}:143/?starttls=when-available")); + AssertDefaultValues (host, 993, SecureSocketOptions.StartTlsWhenAvailable, new Uri ($"imap://{host}:993/?starttls=when-available")); + + AssertDefaultValues (host, 0, SecureSocketOptions.Auto, new Uri ($"imap://{host}:143/?starttls=when-available")); + AssertDefaultValues (host, 143, SecureSocketOptions.Auto, new Uri ($"imap://{host}:143/?starttls=when-available")); + AssertDefaultValues (host, 993, SecureSocketOptions.Auto, new Uri ($"imaps://{host}:993")); } - static void AssertFolder (IMailFolder folder, string fullName, FolderAttributes attributes, bool subscribed, ulong highestmodseq, int count, int recent, uint uidnext, uint validity, int unread) + static Socket Connect (string host, int port) { - if (subscribed) - attributes |= FolderAttributes.Subscribed; + var ipAddresses = Dns.GetHostAddresses (host); + Socket socket = null; + + for (int i = 0; i < ipAddresses.Length; i++) { + socket = new Socket (ipAddresses[i].AddressFamily, SocketType.Stream, ProtocolType.Tcp); + + try { + socket.Connect (ipAddresses[i], port); + break; + } catch { + socket.Dispose (); + socket = null; + } + } - Assert.AreEqual (fullName, folder.FullName, "FullName"); - Assert.AreEqual (attributes, folder.Attributes, "Attributes"); - Assert.AreEqual (subscribed, folder.IsSubscribed, "IsSubscribed"); - Assert.AreEqual (highestmodseq, folder.HighestModSeq, "HighestModSeq"); - Assert.AreEqual (count, folder.Count, "Count"); - Assert.AreEqual (recent, folder.Recent, "Recent"); - Assert.AreEqual (unread, folder.Unread, "Unread"); - Assert.AreEqual (uidnext, folder.UidNext.HasValue ? folder.UidNext.Value.Id : (uint) 0, "UidNext"); - Assert.AreEqual (validity, folder.UidValidity, "UidValidity"); + return socket; } [Test] - public async void TestImapClientGetFolders () + public void TestSslHandshakeExceptions () { - var commands = new List (); - commands.Add (new ImapReplayCommand ("", "gmail.greeting.txt")); - commands.Add (new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt")); - commands.Add (new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "gmail.authenticate.txt")); - commands.Add (new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt")); - commands.Add (new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\"\r\n", "gmail.list-inbox.txt")); - commands.Add (new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt")); - commands.Add (new ImapReplayCommand ("A00000005 LIST (SUBSCRIBED) \"\" \"*\" RETURN (CHILDREN STATUS (MESSAGES RECENT UIDNEXT UIDVALIDITY UNSEEN HIGHESTMODSEQ))\r\n", "gmail.list-all.txt")); - using (var client = new ImapClient ()) { + Socket socket; + + // 1. Test connecting to a non-SSL port fails with an SslHandshakeException. + Assert.Throws (() => client.Connect ("www.gmail.com", 80, true)); + + socket = Connect ("www.gmail.com", 80); + Assert.Throws (() => client.Connect (socket, "www.gmail.com", 80, SecureSocketOptions.SslOnConnect)); + + // 2. Test connecting to a server with a bad SSL certificate fails with an SslHandshakeException. + try { + client.Connect ("untrusted-root.badssl.com", 443, SecureSocketOptions.SslOnConnect); + Assert.Fail ("SSL handshake should have failed with untrusted-root.badssl.com."); + } catch (SslHandshakeException ex) { + Assert.That (ex.ServerCertificate, Is.Not.Null, "ServerCertificate"); + SslHandshakeExceptionTests.AssertBadSslUntrustedRootServerCertificate ((X509Certificate2) ex.ServerCertificate); + + // Note: This is null on Mono because Mono provides an empty chain. + if (ex.RootCertificateAuthority is X509Certificate2 root) + SslHandshakeExceptionTests.AssertBadSslUntrustedRootCACertificate (root); + } catch (Exception ex) { + Assert.Ignore ($"SSL handshake failure inconclusive: {ex}"); + } + try { - client.ReplayConnect ("localhost", new ImapReplayStream (commands, false)); + socket = Connect ("untrusted-root.badssl.com", 443); + client.Connect (socket, "untrusted-root.badssl.com", 443, SecureSocketOptions.SslOnConnect); + Assert.Fail ("SSL handshake should have failed with untrusted-root.badssl.com."); + } catch (SslHandshakeException ex) { + Assert.That (ex.ServerCertificate, Is.Not.Null, "ServerCertificate"); + SslHandshakeExceptionTests.AssertBadSslUntrustedRootServerCertificate ((X509Certificate2) ex.ServerCertificate); + + // Note: This is null on Mono because Mono provides an empty chain. + if (ex.RootCertificateAuthority is X509Certificate2 root) + SslHandshakeExceptionTests.AssertBadSslUntrustedRootCACertificate (root); } catch (Exception ex) { - Assert.Fail ("Did not expect an exception in Connect: {0}", ex); + Assert.Ignore ($"SSL handshake failure inconclusive: {ex}"); } + } + } + + [Test] + public async Task TestSslHandshakeExceptionsAsync () + { + using (var client = new ImapClient ()) { + Socket socket; - Assert.IsTrue (client.IsConnected, "Client failed to connect."); + // 1. Test connecting to a non-SSL port fails with an SslHandshakeException. + Assert.ThrowsAsync (async () => await client.ConnectAsync ("www.gmail.com", 80, true)); - Assert.AreEqual (GMailInitialCapabilities, client.Capabilities); - Assert.AreEqual (5, client.AuthenticationMechanisms.Count); - Assert.IsTrue (client.AuthenticationMechanisms.Contains ("XOAUTH"), "Expected SASL XOAUTH auth mechanism"); - Assert.IsTrue (client.AuthenticationMechanisms.Contains ("XOAUTH2"), "Expected SASL XOAUTH2 auth mechanism"); - Assert.IsTrue (client.AuthenticationMechanisms.Contains ("OAUTHBEARER"), "Expected SASL OAUTHBEARER auth mechanism"); - Assert.IsTrue (client.AuthenticationMechanisms.Contains ("PLAIN"), "Expected SASL PLAIN auth mechanism"); - Assert.IsTrue (client.AuthenticationMechanisms.Contains ("PLAIN-CLIENTTOKEN"), "Expected SASL PLAIN-CLIENTTOKEN auth mechanism"); + socket = Connect ("www.gmail.com", 80); + Assert.ThrowsAsync (async () => await client.ConnectAsync (socket, "www.gmail.com", 80, SecureSocketOptions.SslOnConnect)); - // Note: Do not try XOAUTH2 - client.AuthenticationMechanisms.Remove ("XOAUTH2"); + // 2. Test connecting to a server with a bad SSL certificate fails with an SslHandshakeException. + try { + await client.ConnectAsync ("untrusted-root.badssl.com", 443, SecureSocketOptions.SslOnConnect); + Assert.Fail ("SSL handshake should have failed with untrusted-root.badssl.com."); + } catch (SslHandshakeException ex) { + Assert.That (ex.ServerCertificate, Is.Not.Null, "ServerCertificate"); + SslHandshakeExceptionTests.AssertBadSslUntrustedRootServerCertificate ((X509Certificate2) ex.ServerCertificate); + + // Note: This is null on Mono because Mono provides an empty chain. + if (ex.RootCertificateAuthority is X509Certificate2 root) + SslHandshakeExceptionTests.AssertBadSslUntrustedRootCACertificate (root); + } catch (Exception ex) { + Assert.Ignore ($"SSL handshake failure inconclusive: {ex}"); + } try { - await client.AuthenticateAsync ("username", "password"); + socket = Connect ("untrusted-root.badssl.com", 443); + await client.ConnectAsync (socket, "untrusted-root.badssl.com", 443, SecureSocketOptions.SslOnConnect); + Assert.Fail ("SSL handshake should have failed with untrusted-root.badssl.com."); + } catch (SslHandshakeException ex) { + Assert.That (ex.ServerCertificate, Is.Not.Null, "ServerCertificate"); + SslHandshakeExceptionTests.AssertBadSslUntrustedRootServerCertificate ((X509Certificate2) ex.ServerCertificate); + + // Note: This is null on Mono because Mono provides an empty chain. + if (ex.RootCertificateAuthority is X509Certificate2 root) + SslHandshakeExceptionTests.AssertBadSslUntrustedRootCACertificate (root); } catch (Exception ex) { - Assert.Fail ("Did not expect an exception in Authenticate: {0}", ex); + Assert.Ignore ($"SSL handshake failure inconclusive: {ex}"); } + } + } - Assert.AreEqual (GMailAuthenticatedCapabilities, client.Capabilities); + [Test] + public void TestStartTlsNotSupported () + { + var commands = new List { + new ImapReplayCommand ("", "common.basic-greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "common.capability.txt"), + }; - var all = StatusItems.Count | StatusItems.HighestModSeq | StatusItems.Recent | StatusItems.UidNext | StatusItems.UidValidity | StatusItems.Unread; - var folders = (await client.GetFoldersAsync (client.PersonalNamespaces[0], all, true)).ToList (); - Assert.AreEqual (9, folders.Count, "Unexpected folder count."); + using (var client = new ImapClient () { TagPrefix = 'A' }) + Assert.Throws (() => client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.StartTls), "STARTTLS"); - AssertFolder (folders[0], "INBOX", FolderAttributes.HasNoChildren | FolderAttributes.Inbox, true, 41234, 60, 0, 410, 1, 0); - AssertFolder (folders[1], "[Gmail]", FolderAttributes.HasChildren | FolderAttributes.NonExistent | FolderAttributes.NoSelect, true, 0, 0, 0, 0, 0, 0); - AssertFolder (folders[2], "[Gmail]/All Mail", FolderAttributes.HasNoChildren | FolderAttributes.All, true, 41234, 67, 0, 1210, 11, 3); - AssertFolder (folders[3], "[Gmail]/Drafts", FolderAttributes.HasNoChildren | FolderAttributes.Drafts, true, 41234, 0, 0, 1, 6, 0); - AssertFolder (folders[4], "[Gmail]/Important", FolderAttributes.HasNoChildren | FolderAttributes.Flagged, true, 41234, 58, 0, 307, 9, 0); - AssertFolder (folders[5], "[Gmail]/Sent Mail", FolderAttributes.HasNoChildren | FolderAttributes.Sent, true, 41234, 4, 0, 7, 5, 0); - AssertFolder (folders[6], "[Gmail]/Spam", FolderAttributes.HasNoChildren | FolderAttributes.Junk, true, 41234, 0, 0, 1, 3, 0); - AssertFolder (folders[7], "[Gmail]/Starred", FolderAttributes.HasNoChildren | FolderAttributes.Flagged, true, 41234, 1, 0, 7, 4, 0); - AssertFolder (folders[8], "[Gmail]/Trash", FolderAttributes.HasNoChildren | FolderAttributes.Trash, true, 41234, 0, 0, 1143, 2, 0); + using (var client = new ImapClient () { TagPrefix = 'A' }) + Assert.ThrowsAsync (() => client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.StartTls), "STARTTLS Async"); + } - AssertFolder (client.Inbox, "INBOX", FolderAttributes.HasNoChildren | FolderAttributes.Inbox, true, 41234, 60, 0, 410, 1, 0); - AssertFolder (client.GetFolder (SpecialFolder.All), "[Gmail]/All Mail", FolderAttributes.HasNoChildren | FolderAttributes.All, true, 41234, 67, 0, 1210, 11, 3); - AssertFolder (client.GetFolder (SpecialFolder.Drafts), "[Gmail]/Drafts", FolderAttributes.HasNoChildren | FolderAttributes.Drafts, true, 41234, 0, 0, 1, 6, 0); - //AssertFolder (client.GetFolder (SpecialFolder.Flagged), "[Gmail]/Important", FolderAttributes.HasNoChildren | FolderAttributes.Flagged, true, 41234, 58, 0, 307, 9, 0); - AssertFolder (client.GetFolder (SpecialFolder.Sent), "[Gmail]/Sent Mail", FolderAttributes.HasNoChildren | FolderAttributes.Sent, true, 41234, 4, 0, 7, 5, 0); - AssertFolder (client.GetFolder (SpecialFolder.Junk), "[Gmail]/Spam", FolderAttributes.HasNoChildren | FolderAttributes.Junk, true, 41234, 0, 0, 1, 3, 0); - AssertFolder (client.GetFolder (SpecialFolder.Flagged), "[Gmail]/Starred", FolderAttributes.HasNoChildren | FolderAttributes.Flagged, true, 41234, 1, 0, 7, 4, 0); - AssertFolder (client.GetFolder (SpecialFolder.Trash), "[Gmail]/Trash", FolderAttributes.HasNoChildren | FolderAttributes.Trash, true, 41234, 0, 0, 1143, 2, 0); + [Test] + public void TestProtocolLoggerExceptions () + { + var commands = new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt"), + }; - client.Disconnect (false); - } + using (var client = new ImapClient (new ExceptionalProtocolLogger (ExceptionalProtocolLoggerMode.ThrowOnLogConnect)) { TagPrefix = 'A' }) + Assert.Throws (() => client.Connect (Stream.Null, "imap.gmail.com", 143, SecureSocketOptions.None), "LogConnect"); + + using (var client = new ImapClient (new ExceptionalProtocolLogger (ExceptionalProtocolLoggerMode.ThrowOnLogConnect)) { TagPrefix = 'A' }) + Assert.ThrowsAsync (() => client.ConnectAsync (Stream.Null, "imap.gmail.com", 143, SecureSocketOptions.None), "LogConnect Async"); + + using (var client = new ImapClient (new ExceptionalProtocolLogger (ExceptionalProtocolLoggerMode.ThrowOnLogServer)) { TagPrefix = 'A' }) + Assert.Throws (() => client.Connect (new ImapReplayStream (commands, false), "imap.gmail.com", 143, SecureSocketOptions.None), "LogServer"); + + using (var client = new ImapClient (new ExceptionalProtocolLogger (ExceptionalProtocolLoggerMode.ThrowOnLogServer)) { TagPrefix = 'A' }) + Assert.ThrowsAsync (() => client.ConnectAsync (new ImapReplayStream (commands, true), "imap.gmail.com", 143, SecureSocketOptions.None), "LogServer Async"); + + using (var client = new ImapClient (new ExceptionalProtocolLogger (ExceptionalProtocolLoggerMode.ThrowOnLogClient)) { TagPrefix = 'A' }) + Assert.Throws (() => client.Connect (new ImapReplayStream (commands, false), "imap.gmail.com", 143, SecureSocketOptions.None), "LogClient"); + + using (var client = new ImapClient (new ExceptionalProtocolLogger (ExceptionalProtocolLoggerMode.ThrowOnLogClient)) { TagPrefix = 'A' }) + Assert.ThrowsAsync (() => client.ConnectAsync (new ImapReplayStream (commands, true), "imap.gmail.com", 143, SecureSocketOptions.None), "LogClient Async"); + } + + static void AssertGMailIsConnected (IMailService client) + { + Assert.That (client.IsConnected, Is.True, "Expected the client to be connected"); + Assert.That (client.IsSecure, Is.True, "Expected a secure connection"); + Assert.That (client.IsEncrypted, Is.True, "Expected an encrypted connection"); + Assert.That (client.IsSigned, Is.True, "Expected a signed connection"); + Assert.That (client.SslProtocol == SslProtocols.Tls12 || client.SslProtocol == SslProtocols.Tls13, Is.True, "Expected a TLS v1.2 or TLS v1.3 connection"); + Assert.That (client.SslCipherAlgorithm == CipherAlgorithmType.Aes128 || client.SslCipherAlgorithm == CipherAlgorithmType.Aes256, Is.True, $"Unexpected SslCipherAlgorithm: {client.SslCipherAlgorithm}"); + Assert.That (client.SslCipherStrength == 128 || client.SslCipherStrength == 256, Is.True, $"Unexpected SslCipherStrength: {client.SslCipherStrength}"); +#if !MONO + Assert.That (client.SslCipherSuite == TlsCipherSuite.TLS_AES_128_GCM_SHA256 || client.SslCipherSuite == TlsCipherSuite.TLS_AES_256_GCM_SHA384, Is.True, $"Unexpected SslCipherSuite: {client.SslCipherSuite}"); + Assert.That (client.SslHashAlgorithm == HashAlgorithmType.Sha256 || client.SslHashAlgorithm == HashAlgorithmType.Sha384, Is.True, $"Unexpected SslHashAlgorithm: {client.SslHashAlgorithm}"); +#else + Assert.That (client.SslHashAlgorithm == HashAlgorithmType.None, Is.True, $"Unexpected SslHashAlgorithm: {client.SslHashAlgorithm}"); +#endif + + Assert.That (client.SslHashStrength, Is.EqualTo (0), $"Unexpected SslHashStrength: {client.SslHashStrength}"); + Assert.That (client.SslKeyExchangeAlgorithm == ExchangeAlgorithmType.None || client.SslKeyExchangeAlgorithm == EcdhEphemeral, Is.True, $"Unexpected SslKeyExchangeAlgorithm: {client.SslKeyExchangeAlgorithm}"); + Assert.That (client.SslKeyExchangeStrength, Is.AnyOf (0, 255, 256, 384), $"Unexpected SslKeyExchangeStrength: {client.SslKeyExchangeStrength}"); + Assert.That (client.IsAuthenticated, Is.False, "Expected the client to not be authenticated"); + } + + static void AssertClientIsDisconnected (IMailService client) + { + Assert.That (client.IsConnected, Is.False, "Expected the client to be disconnected"); + Assert.That (client.IsSecure, Is.False, "Expected IsSecure to be false after disconnecting"); + Assert.That (client.IsEncrypted, Is.False, "Expected IsEncrypted to be false after disconnecting"); + Assert.That (client.IsSigned, Is.False, "Expected IsSigned to be false after disconnecting"); + Assert.That (client.SslProtocol, Is.EqualTo (SslProtocols.None), "Expected SslProtocol to be None after disconnecting"); + Assert.That (client.SslCipherAlgorithm, Is.Null, "Expected SslCipherAlgorithm to be null after disconnecting"); + Assert.That (client.SslCipherStrength, Is.Null, "Expected SslCipherStrength to be null after disconnecting"); + Assert.That (client.SslCipherSuite, Is.Null, "Expected SslCipherSuite to be null after disconnecting"); + Assert.That (client.SslHashAlgorithm, Is.Null, "Expected SslHashAlgorithm to be null after disconnecting"); + Assert.That (client.SslHashStrength, Is.Null, "Expected SslHashStrength to be null after disconnecting"); + Assert.That (client.SslKeyExchangeAlgorithm, Is.Null, "Expected SslKeyExchangeAlgorithm to be null after disconnecting"); + Assert.That (client.SslKeyExchangeStrength, Is.Null, "Expected SslKeyExchangeStrength to be null after disconnecting"); } [Test] - public async void TestGetQuotaNonexistantQuotaRoot () + public void TestConnectGMail () { - var commands = new List (); - commands.Add (new ImapReplayCommand ("", "gmail.greeting.txt")); - commands.Add (new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt")); - commands.Add (new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "gmail.authenticate.txt")); - commands.Add (new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt")); - commands.Add (new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\"\r\n", "gmail.list-inbox.txt")); - commands.Add (new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt")); - commands.Add (new ImapReplayCommand ("A00000005 GETQUOTAROOT INBOX\r\n", "common.getquota-no-root.txt")); - commands.Add (new ImapReplayCommand ("A00000006 LIST \"\" storage=0\r\n", ImapReplayCommandResponse.OK)); + var options = SecureSocketOptions.SslOnConnect; + var host = "imap.gmail.com"; + int port = 993; using (var client = new ImapClient ()) { - try { - client.ReplayConnect ("localhost", new ImapReplayStream (commands, false)); - } catch (Exception ex) { - Assert.Fail ("Did not expect an exception in Connect: {0}", ex); - } + int connected = 0, disconnected = 0; + + client.Connected += (sender, e) => { + Assert.That (e.Host, Is.EqualTo (host), "ConnectedEventArgs.Host"); + Assert.That (e.Port, Is.EqualTo (port), "ConnectedEventArgs.Port"); + Assert.That (e.Options, Is.EqualTo (options), "ConnectedEventArgs.Options"); + connected++; + }; - Assert.IsTrue (client.IsConnected, "Client failed to connect."); + client.Disconnected += (sender, e) => { + Assert.That (e.Host, Is.EqualTo (host), "DisconnectedEventArgs.Host"); + Assert.That (e.Port, Is.EqualTo (port), "DisconnectedEventArgs.Port"); + Assert.That (e.Options, Is.EqualTo (options), "DisconnectedEventArgs.Options"); + Assert.That (e.IsRequested, Is.True, "DisconnectedEventArgs.IsRequested"); + disconnected++; + }; - Assert.AreEqual (GMailInitialCapabilities, client.Capabilities); - Assert.AreEqual (5, client.AuthenticationMechanisms.Count); - Assert.IsTrue (client.AuthenticationMechanisms.Contains ("XOAUTH"), "Expected SASL XOAUTH auth mechanism"); - Assert.IsTrue (client.AuthenticationMechanisms.Contains ("XOAUTH2"), "Expected SASL XOAUTH2 auth mechanism"); - Assert.IsTrue (client.AuthenticationMechanisms.Contains ("OAUTHBEARER"), "Expected SASL OAUTHBEARER auth mechanism"); - Assert.IsTrue (client.AuthenticationMechanisms.Contains ("PLAIN"), "Expected SASL PLAIN auth mechanism"); - Assert.IsTrue (client.AuthenticationMechanisms.Contains ("PLAIN-CLIENTTOKEN"), "Expected SASL PLAIN-CLIENTTOKEN auth mechanism"); + client.Connect (host, 0, options); + AssertGMailIsConnected (client); + Assert.That (connected, Is.EqualTo (1), "ConnectedEvent"); - // Note: Do not try XOAUTH2 - client.AuthenticationMechanisms.Remove ("XOAUTH2"); + Assert.Throws (() => client.Connect (host, 0, options)); - try { - await client.AuthenticateAsync ("username", "password"); - } catch (Exception ex) { - Assert.Fail ("Did not expect an exception in Authenticate: {0}", ex); - } + client.Disconnect (true); + AssertClientIsDisconnected (client); + Assert.That (disconnected, Is.EqualTo (1), "DisconnectedEvent"); + } + } - Assert.AreEqual (GMailAuthenticatedCapabilities, client.Capabilities); + [Test] + public async Task TestConnectGMailAsync () + { + var options = SecureSocketOptions.SslOnConnect; + var host = "imap.gmail.com"; + int port = 993; - var inbox = client.Inbox; + using (var client = new ImapClient ()) { + int connected = 0, disconnected = 0; - Assert.IsNotNull (inbox, "Expected non-null Inbox folder."); - Assert.AreEqual (FolderAttributes.Inbox | FolderAttributes.HasNoChildren, inbox.Attributes, "Expected Inbox attributes to be \\HasNoChildren."); + client.Connected += (sender, e) => { + Assert.That (e.Host, Is.EqualTo (host), "ConnectedEventArgs.Host"); + Assert.That (e.Port, Is.EqualTo (port), "ConnectedEventArgs.Port"); + Assert.That (e.Options, Is.EqualTo (options), "ConnectedEventArgs.Options"); + connected++; + }; - var quota = await inbox.GetQuotaAsync (); - Assert.IsNotNull (quota, "Expected a non-null GETQUOTAROOT response."); - Assert.IsFalse (quota.QuotaRoot.Exists); - Assert.AreEqual ("storage=0", quota.QuotaRoot.FullName); - Assert.AreEqual (28257, quota.CurrentStorageSize.Value); - Assert.AreEqual (256000, quota.StorageLimit.Value); - Assert.IsFalse (quota.CurrentMessageCount.HasValue); - Assert.IsFalse (quota.MessageLimit.HasValue); + client.Disconnected += (sender, e) => { + Assert.That (e.Host, Is.EqualTo (host), "DisconnectedEventArgs.Host"); + Assert.That (e.Port, Is.EqualTo (port), "DisconnectedEventArgs.Port"); + Assert.That (e.Options, Is.EqualTo (options), "DisconnectedEventArgs.Options"); + Assert.That (e.IsRequested, Is.True, "DisconnectedEventArgs.IsRequested"); + disconnected++; + }; - await client.DisconnectAsync (false); + await client.ConnectAsync (host, 0, options); + AssertGMailIsConnected (client); + Assert.That (connected, Is.EqualTo (1), "ConnectedEvent"); + + Assert.ThrowsAsync (async () => await client.ConnectAsync (host, 0, options)); + + await client.DisconnectAsync (true); + AssertClientIsDisconnected (client); + Assert.That (disconnected, Is.EqualTo (1), "DisconnectedEvent"); } } - static MimeMessage CreateThreadableMessage (string subject, string msgid, string references, DateTimeOffset date) + [Test] + public void TestConnectGMailViaProxy () { - var message = new MimeMessage (); - message.From.Add (new MailboxAddress ("Unit Tests", "unit-tests@mimekit.net")); - message.To.Add (new MailboxAddress ("Unit Tests", "unit-tests@mimekit.net")); - message.MessageId = msgid; - message.Subject = subject; - message.Date = date; + var options = SecureSocketOptions.SslOnConnect; + var host = "imap.gmail.com"; + int port = 993; + + using (var proxy = new Socks5ProxyListener ()) { + proxy.Start (IPAddress.Loopback, 0); + + using (var client = new ImapClient ()) { + int connected = 0, disconnected = 0; + + client.Connected += (sender, e) => { + Assert.That (e.Host, Is.EqualTo (host), "ConnectedEventArgs.Host"); + Assert.That (e.Port, Is.EqualTo (port), "ConnectedEventArgs.Port"); + Assert.That (e.Options, Is.EqualTo (options), "ConnectedEventArgs.Options"); + connected++; + }; + + client.Disconnected += (sender, e) => { + Assert.That (e.Host, Is.EqualTo (host), "DisconnectedEventArgs.Host"); + Assert.That (e.Port, Is.EqualTo (port), "DisconnectedEventArgs.Port"); + Assert.That (e.Options, Is.EqualTo (options), "DisconnectedEventArgs.Options"); + Assert.That (e.IsRequested, Is.True, "DisconnectedEventArgs.IsRequested"); + disconnected++; + }; + + client.ProxyClient = new Socks5Client (proxy.IPAddress.ToString (), proxy.Port); + client.ServerCertificateValidationCallback = (s, c, h, e) => true; + client.ClientCertificates = null; + client.LocalEndPoint = null; + client.Timeout = 20000; + + try { + client.Connect (host, 0, options); + } catch (TimeoutException) { + Assert.Inconclusive ("Timed out."); + return; + } catch (Exception ex) { + Assert.Fail (ex.Message); + } + AssertGMailIsConnected (client); + Assert.That (connected, Is.EqualTo (1), "ConnectedEvent"); - if (references != null) { - foreach (var reference in references.Split (' ')) - message.References.Add (reference); + Assert.Throws (() => client.Connect (host, 0, options)); + + client.Disconnect (true); + AssertClientIsDisconnected (client); + Assert.That (disconnected, Is.EqualTo (1), "DisconnectedEvent"); + } } + } - message.Body = new TextPart ("plain") { Text = "This is the message body.\r\n" }; + [Test] + public async Task TestConnectGMailViaProxyAsync () + { + var options = SecureSocketOptions.SslOnConnect; + var host = "imap.gmail.com"; + int port = 993; + + using (var proxy = new Socks5ProxyListener ()) { + proxy.Start (IPAddress.Loopback, 0); + + using (var client = new ImapClient ()) { + int connected = 0, disconnected = 0; + + client.Connected += (sender, e) => { + Assert.That (e.Host, Is.EqualTo (host), "ConnectedEventArgs.Host"); + Assert.That (e.Port, Is.EqualTo (port), "ConnectedEventArgs.Port"); + Assert.That (e.Options, Is.EqualTo (options), "ConnectedEventArgs.Options"); + connected++; + }; + + client.Disconnected += (sender, e) => { + Assert.That (e.Host, Is.EqualTo (host), "DisconnectedEventArgs.Host"); + Assert.That (e.Port, Is.EqualTo (port), "DisconnectedEventArgs.Port"); + Assert.That (e.Options, Is.EqualTo (options), "DisconnectedEventArgs.Options"); + Assert.That (e.IsRequested, Is.True, "DisconnectedEventArgs.IsRequested"); + disconnected++; + }; + + + client.ProxyClient = new Socks5Client (proxy.IPAddress.ToString (), proxy.Port); + client.ServerCertificateValidationCallback = (s, c, h, e) => true; + client.ClientCertificates = null; + client.LocalEndPoint = null; + client.Timeout = 20000; + + try { + await client.ConnectAsync (host, 0, options); + } catch (TimeoutException) { + Assert.Inconclusive ("Timed out."); + return; + } catch (Exception ex) { + Assert.Fail (ex.Message); + } + AssertGMailIsConnected (client); + Assert.That (connected, Is.EqualTo (1), "ConnectedEvent"); - return message; + Assert.ThrowsAsync (async () => await client.ConnectAsync (host, 0, options)); + + await client.DisconnectAsync (true); + AssertClientIsDisconnected (client); + Assert.That (disconnected, Is.EqualTo (1), "DisconnectedEvent"); + } + } } [Test] - public async void TestImapClientDovecot () + public void TestConnectGMailSocket () { - var expectedFlags = MessageFlags.Answered | MessageFlags.Flagged | MessageFlags.Deleted | MessageFlags.Seen | MessageFlags.Draft; - var expectedPermanentFlags = expectedFlags | MessageFlags.UserDefined; - - var commands = new List (); - commands.Add (new ImapReplayCommand ("", "dovecot.greeting.txt")); - commands.Add (new ImapReplayCommand ("A00000000 LOGIN username password\r\n", "dovecot.authenticate.txt")); - commands.Add (new ImapReplayCommand ("A00000001 NAMESPACE\r\n", "dovecot.namespace.txt")); - commands.Add (new ImapReplayCommand ("A00000002 LIST \"\" \"INBOX\"\r\n", "dovecot.list-inbox.txt")); - commands.Add (new ImapReplayCommand ("A00000003 LIST (SPECIAL-USE) \"\" \"*\"\r\n", "dovecot.list-special-use.txt")); - commands.Add (new ImapReplayCommand ("A00000004 ENABLE QRESYNC CONDSTORE\r\n", "dovecot.enable-qresync.txt")); - commands.Add (new ImapReplayCommand ("A00000005 LIST \"\" \"%\" RETURN (SUBSCRIBED CHILDREN STATUS (MESSAGES RECENT UIDNEXT UIDVALIDITY UNSEEN HIGHESTMODSEQ))\r\n", "dovecot.list-personal.txt")); - commands.Add (new ImapReplayCommand ("A00000006 CREATE UnitTests.\r\n", ImapReplayCommandResponse.OK)); - commands.Add (new ImapReplayCommand ("A00000007 LIST \"\" UnitTests\r\n", "dovecot.list-unittests.txt")); - commands.Add (new ImapReplayCommand ("A00000008 CREATE UnitTests.Messages\r\n", ImapReplayCommandResponse.OK)); - commands.Add (new ImapReplayCommand ("A00000009 LIST \"\" UnitTests.Messages\r\n", "dovecot.list-unittests-messages.txt")); + var options = SecureSocketOptions.SslOnConnect; + var host = "imap.gmail.com"; + int port = 993; - var command = new StringBuilder ("A00000010 APPEND UnitTests.Messages"); - var internalDates = new List (); - var messages = new List (); - var flags = new List (); - var now = DateTimeOffset.Now; + using (var client = new ImapClient ()) { + int connected = 0, disconnected = 0; - messages.Add (CreateThreadableMessage ("A", "", null, now.AddMinutes (-7))); - messages.Add (CreateThreadableMessage ("B", "", "", now.AddMinutes (-6))); - messages.Add (CreateThreadableMessage ("C", "", " ", now.AddMinutes (-5))); - messages.Add (CreateThreadableMessage ("D", "", "", now.AddMinutes (-4))); - messages.Add (CreateThreadableMessage ("E", "", " ", now.AddMinutes (-3))); - messages.Add (CreateThreadableMessage ("F", "", "", now.AddMinutes (-2))); - messages.Add (CreateThreadableMessage ("G", "", null, now.AddMinutes (-1))); - messages.Add (CreateThreadableMessage ("H", "", null, now)); + client.Connected += (sender, e) => { + Assert.That (e.Host, Is.EqualTo (host), "ConnectedEventArgs.Host"); + Assert.That (e.Port, Is.EqualTo (port), "ConnectedEventArgs.Port"); + Assert.That (e.Options, Is.EqualTo (options), "ConnectedEventArgs.Options"); + connected++; + }; - for (int i = 0; i < messages.Count; i++) { - var message = messages[i]; - string latin1; - long length; + client.Disconnected += (sender, e) => { + Assert.That (e.Host, Is.EqualTo (host), "DisconnectedEventArgs.Host"); + Assert.That (e.Port, Is.EqualTo (port), "DisconnectedEventArgs.Port"); + Assert.That (e.Options, Is.EqualTo (options), "DisconnectedEventArgs.Options"); + Assert.That (e.IsRequested, Is.True, "DisconnectedEventArgs.IsRequested"); + disconnected++; + }; - internalDates.Add (messages[i].Date); - flags.Add (MessageFlags.Draft); + var socket = Connect (host, port); - using (var stream = new MemoryStream ()) { - var options = FormatOptions.Default.Clone (); - options.NewLineFormat = NewLineFormat.Dos; + Assert.Throws (() => client.Connect (socket, null, port, SecureSocketOptions.Auto)); + Assert.Throws (() => client.Connect (socket, "", port, SecureSocketOptions.Auto)); + Assert.Throws (() => client.Connect (socket, host, -1, SecureSocketOptions.Auto)); - message.WriteTo (options, stream); - length = stream.Length; - stream.Position = 0; + client.Connect (socket, host, port, SecureSocketOptions.Auto); + AssertGMailIsConnected (client); + Assert.That (connected, Is.EqualTo (1), "ConnectedEvent"); - using (var reader = new StreamReader (stream, Latin1)) - latin1 = reader.ReadToEnd (); - } + Assert.Throws (() => client.Connect (socket, host, port, SecureSocketOptions.Auto)); - command.AppendFormat (" (\\Draft) \"{0}\" ", ImapUtils.FormatInternalDate (message.Date)); - command.Append ('{'); - command.AppendFormat ("{0}+", length); - command.Append ("}\r\n"); - command.Append (latin1); + client.Disconnect (true); + AssertClientIsDisconnected (client); + Assert.That (disconnected, Is.EqualTo (1), "DisconnectedEvent"); } - command.Append ("\r\n"); - commands.Add (new ImapReplayCommand (command.ToString (), "dovecot.multiappend.txt")); - commands.Add (new ImapReplayCommand ("A00000011 SELECT UnitTests.Messages (CONDSTORE)\r\n", "dovecot.select-unittests-messages.txt")); - commands.Add (new ImapReplayCommand ("A00000012 UID STORE 1:8 +FLAGS.SILENT (\\Seen)\r\n", "dovecot.store-seen.txt")); - commands.Add (new ImapReplayCommand ("A00000013 UID STORE 1:3 +FLAGS.SILENT (\\Answered)\r\n", "dovecot.store-answered.txt")); - commands.Add (new ImapReplayCommand ("A00000014 UID STORE 8 +FLAGS.SILENT (\\Deleted)\r\n", "dovecot.store-deleted.txt")); - commands.Add (new ImapReplayCommand ("A00000015 UID EXPUNGE 8\r\n", "dovecot.uid-expunge.txt")); - commands.Add (new ImapReplayCommand ("A00000016 UID THREAD REFERENCES US-ASCII ALL\r\n", "dovecot.thread-references.txt")); - commands.Add (new ImapReplayCommand ("A00000017 UID THREAD ORDEREDSUBJECT US-ASCII UID 1:* ALL\r\n", "dovecot.thread-orderedsubject.txt")); - commands.Add (new ImapReplayCommand ("A00000018 UNSELECT\r\n", ImapReplayCommandResponse.OK)); - commands.Add (new ImapReplayCommand ("A00000019 SELECT UnitTests.Messages (QRESYNC (1436832084 2 1:8))\r\n", "dovecot.select-unittests-messages-qresync.txt")); - commands.Add (new ImapReplayCommand ("A00000020 UID SEARCH RETURN (ALL COUNT MIN MAX) MODSEQ 2\r\n", "dovecot.search-changed-since.txt")); - commands.Add (new ImapReplayCommand ("A00000021 UID FETCH 1:7 (UID FLAGS MODSEQ)\r\n", "dovecot.fetch1.txt")); - commands.Add (new ImapReplayCommand ("A00000022 UID FETCH 1:* (UID FLAGS MODSEQ) (CHANGEDSINCE 2 VANISHED)\r\n", "dovecot.fetch2.txt")); - commands.Add (new ImapReplayCommand ("A00000023 UID SORT RETURN (ALL COUNT MIN MAX) (REVERSE ARRIVAL) US-ASCII ALL\r\n", "dovecot.sort-reverse-arrival.txt")); - commands.Add (new ImapReplayCommand ("A00000024 UID SEARCH RETURN () UNDELETED SEEN\r\n", "dovecot.optimized-search.txt")); - commands.Add (new ImapReplayCommand ("A00000025 CREATE UnitTests.Destination\r\n", ImapReplayCommandResponse.OK)); - commands.Add (new ImapReplayCommand ("A00000026 LIST \"\" UnitTests.Destination\r\n", "dovecot.list-unittests-destination.txt")); - commands.Add (new ImapReplayCommand ("A00000027 UID COPY 1:7 UnitTests.Destination\r\n", "dovecot.copy.txt")); - commands.Add (new ImapReplayCommand ("A00000028 UID MOVE 1:7 UnitTests.Destination\r\n", "dovecot.move.txt")); - commands.Add (new ImapReplayCommand ("A00000029 STATUS UnitTests.Destination (MESSAGES RECENT UIDNEXT UIDVALIDITY UNSEEN HIGHESTMODSEQ)\r\n", "dovecot.status-unittests-destination.txt")); - commands.Add (new ImapReplayCommand ("A00000030 SELECT UnitTests.Destination (CONDSTORE)\r\n", "dovecot.select-unittests-destination.txt")); - commands.Add (new ImapReplayCommand ("A00000031 UID FETCH 1:* (UID FLAGS INTERNALDATE RFC822.SIZE ENVELOPE BODYSTRUCTURE MODSEQ BODY.PEEK[HEADER.FIELDS (REFERENCES X-MAILER)]) (CHANGEDSINCE 1 VANISHED)\r\n", "dovecot.fetch3.txt")); - commands.Add (new ImapReplayCommand ("A00000032 FETCH 1:* (UID FLAGS INTERNALDATE RFC822.SIZE ENVELOPE BODYSTRUCTURE MODSEQ BODY.PEEK[HEADER.FIELDS (REFERENCES X-MAILER)]) (CHANGEDSINCE 1)\r\n", "dovecot.fetch4.txt")); - commands.Add (new ImapReplayCommand ("A00000033 FETCH 1:14 (UID FLAGS INTERNALDATE RFC822.SIZE ENVELOPE BODYSTRUCTURE MODSEQ BODY.PEEK[HEADER.FIELDS (REFERENCES X-MAILER)]) (CHANGEDSINCE 1)\r\n", "dovecot.fetch5.txt")); - commands.Add (new ImapReplayCommand ("A00000034 FETCH 1:* (UID FLAGS INTERNALDATE RFC822.SIZE ENVELOPE BODYSTRUCTURE MODSEQ BODY.PEEK[HEADER.FIELDS (REFERENCES)]) (CHANGEDSINCE 1)\r\n", "dovecot.fetch6.txt")); - commands.Add (new ImapReplayCommand ("A00000035 FETCH 1:14 (UID FLAGS INTERNALDATE RFC822.SIZE ENVELOPE BODYSTRUCTURE MODSEQ BODY.PEEK[HEADER.FIELDS (REFERENCES)]) (CHANGEDSINCE 1)\r\n", "dovecot.fetch7.txt")); - commands.Add (new ImapReplayCommand ("A00000036 UID FETCH 1:* (UID FLAGS INTERNALDATE RFC822.SIZE ENVELOPE BODYSTRUCTURE MODSEQ BODY.PEEK[HEADER.FIELDS (REFERENCES X-MAILER)])\r\n", "dovecot.fetch8.txt")); - commands.Add (new ImapReplayCommand ("A00000037 FETCH 1:* (UID FLAGS INTERNALDATE RFC822.SIZE ENVELOPE BODYSTRUCTURE MODSEQ BODY.PEEK[HEADER.FIELDS (REFERENCES X-MAILER)])\r\n", "dovecot.fetch9.txt")); - commands.Add (new ImapReplayCommand ("A00000038 FETCH 1:14 (UID FLAGS INTERNALDATE RFC822.SIZE ENVELOPE BODYSTRUCTURE MODSEQ BODY.PEEK[HEADER.FIELDS (REFERENCES X-MAILER)])\r\n", "dovecot.fetch10.txt")); - commands.Add (new ImapReplayCommand ("A00000039 FETCH 1:* (UID FLAGS INTERNALDATE RFC822.SIZE ENVELOPE BODYSTRUCTURE MODSEQ BODY.PEEK[HEADER.FIELDS (REFERENCES)])\r\n", "dovecot.fetch11.txt")); - commands.Add (new ImapReplayCommand ("A00000040 FETCH 1:14 (UID FLAGS INTERNALDATE RFC822.SIZE ENVELOPE BODYSTRUCTURE MODSEQ BODY.PEEK[HEADER.FIELDS (REFERENCES)])\r\n", "dovecot.fetch12.txt")); - commands.Add (new ImapReplayCommand ("A00000041 UID FETCH 1 (BODY.PEEK[HEADER] BODY.PEEK[TEXT])\r\n", "dovecot.getbodypart.txt")); - commands.Add (new ImapReplayCommand ("A00000042 FETCH 1 (BODY.PEEK[HEADER] BODY.PEEK[TEXT])\r\n", "dovecot.getbodypart2.txt")); - commands.Add (new ImapReplayCommand ("A00000043 UID FETCH 1 (BODY.PEEK[HEADER])\r\n", "dovecot.getmessageheaders.txt")); - commands.Add (new ImapReplayCommand ("A00000044 FETCH 1 (BODY.PEEK[HEADER])\r\n", "dovecot.getmessageheaders2.txt")); - commands.Add (new ImapReplayCommand ("A00000045 UID FETCH 1 (BODY.PEEK[HEADER])\r\n", "dovecot.getbodypartheaders.txt")); - commands.Add (new ImapReplayCommand ("A00000046 FETCH 1 (BODY.PEEK[HEADER])\r\n", "dovecot.getbodypartheaders2.txt")); - commands.Add (new ImapReplayCommand ("A00000047 UID FETCH 1 (BODY.PEEK[]<128.64>)\r\n", "dovecot.getstream.txt")); - commands.Add (new ImapReplayCommand ("A00000048 UID FETCH 1 (BODY.PEEK[]<128.64>)\r\n", "dovecot.getstream2.txt")); - commands.Add (new ImapReplayCommand ("A00000049 FETCH 1 (BODY.PEEK[]<128.64>)\r\n", "dovecot.getstream3.txt")); - commands.Add (new ImapReplayCommand ("A00000050 FETCH 1 (BODY.PEEK[]<128.64>)\r\n", "dovecot.getstream4.txt")); - commands.Add (new ImapReplayCommand ("A00000051 UID FETCH 1 (BODY.PEEK[HEADER.FIELDS (MIME-VERSION CONTENT-TYPE)])\r\n", "dovecot.getstream-section.txt")); - commands.Add (new ImapReplayCommand ("A00000052 FETCH 1 (BODY.PEEK[HEADER.FIELDS (MIME-VERSION CONTENT-TYPE)])\r\n", "dovecot.getstream-section2.txt")); - commands.Add (new ImapReplayCommand ("A00000053 UID STORE 1:14 (UNCHANGEDSINCE 3) +FLAGS.SILENT (\\Deleted $MailKit)\r\n", "dovecot.store-deleted-custom.txt")); - commands.Add (new ImapReplayCommand ("A00000054 STORE 1:7 (UNCHANGEDSINCE 5) FLAGS.SILENT (\\Deleted \\Seen $MailKit)\r\n", "dovecot.setflags-unchangedsince.txt")); - commands.Add (new ImapReplayCommand ("A00000055 UID SEARCH RETURN () UID 1:14 OR ANSWERED OR DELETED OR DRAFT OR FLAGGED OR RECENT OR UNANSWERED OR UNDELETED OR UNDRAFT OR UNFLAGGED OR UNSEEN OR KEYWORD $MailKit UNKEYWORD $MailKit\r\n", "dovecot.search-uids.txt")); - commands.Add (new ImapReplayCommand ("A00000056 UID SEARCH RETURN (ALL COUNT MIN MAX) UID 1:14 LARGER 256 SMALLER 512\r\n", "dovecot.search-uids-options.txt")); - commands.Add (new ImapReplayCommand ("A00000057 UID SORT RETURN () (REVERSE DATE SUBJECT DISPLAYFROM SIZE) US-ASCII OR OR (SENTBEFORE 12-Oct-2016 SENTSINCE 10-Oct-2016) NOT SENTON 11-Oct-2016 OR (BEFORE 12-Oct-2016 SINCE 10-Oct-2016) NOT ON 11-Oct-2016\r\n", "dovecot.sort-by-date.txt")); - commands.Add (new ImapReplayCommand ("A00000058 UID SORT RETURN () (FROM TO CC) US-ASCII UID 1:14 OR BCC xyz OR CC xyz OR FROM xyz OR TO xyz OR SUBJECT xyz OR HEADER Message-Id mimekit.net OR BODY \"This is the message body.\" TEXT message\r\n", "dovecot.sort-by-strings.txt")); - commands.Add (new ImapReplayCommand ("A00000059 UID SORT RETURN (ALL COUNT MIN MAX) (DISPLAYTO) US-ASCII UID 1:14 OLDER 1 YOUNGER 3600\r\n", "dovecot.sort-uids-options.txt")); - commands.Add (new ImapReplayCommand ("A00000060 UID SEARCH ALL\r\n", "dovecot.search-raw.txt")); - commands.Add (new ImapReplayCommand ("A00000061 UID SORT (REVERSE ARRIVAL) US-ASCII ALL\r\n", "dovecot.sort-raw.txt")); - commands.Add (new ImapReplayCommand ("A00000062 EXPUNGE\r\n", "dovecot.expunge.txt")); - commands.Add (new ImapReplayCommand ("A00000063 CLOSE\r\n", ImapReplayCommandResponse.OK)); + } + + [Test] + public async Task TestConnectGMailSocketAsync () + { + var options = SecureSocketOptions.SslOnConnect; + var host = "imap.gmail.com"; + int port = 993; using (var client = new ImapClient ()) { - try { - client.ReplayConnect ("localhost", new ImapReplayStream (commands, false)); - } catch (Exception ex) { - Assert.Fail ("Did not expect an exception in Connect: {0}", ex); - } + int connected = 0, disconnected = 0; - Assert.IsTrue (client.IsConnected, "Client failed to connect."); + client.Connected += (sender, e) => { + Assert.That (e.Host, Is.EqualTo (host), "ConnectedEventArgs.Host"); + Assert.That (e.Port, Is.EqualTo (port), "ConnectedEventArgs.Port"); + Assert.That (e.Options, Is.EqualTo (options), "ConnectedEventArgs.Options"); + connected++; + }; - Assert.AreEqual (DovecotInitialCapabilities, client.Capabilities); - Assert.AreEqual (4, client.AuthenticationMechanisms.Count); - Assert.IsTrue (client.AuthenticationMechanisms.Contains ("PLAIN"), "Expected SASL PLAIN auth mechanism"); - Assert.IsTrue (client.AuthenticationMechanisms.Contains ("DIGEST-MD5"), "Expected SASL DIGEST-MD5 auth mechanism"); - Assert.IsTrue (client.AuthenticationMechanisms.Contains ("CRAM-MD5"), "Expected SASL CRAM-MD5 auth mechanism"); - Assert.IsTrue (client.AuthenticationMechanisms.Contains ("NTLM"), "Expected SASL NTLM auth mechanism"); + client.Disconnected += (sender, e) => { + Assert.That (e.Host, Is.EqualTo (host), "DisconnectedEventArgs.Host"); + Assert.That (e.Port, Is.EqualTo (port), "DisconnectedEventArgs.Port"); + Assert.That (e.Options, Is.EqualTo (options), "DisconnectedEventArgs.Options"); + Assert.That (e.IsRequested, Is.True, "DisconnectedEventArgs.IsRequested"); + disconnected++; + }; - // Note: we do not want to use SASL at all... - client.AuthenticationMechanisms.Clear (); + var socket = Connect (host, port); - try { - await client.AuthenticateAsync ("username", "password"); - } catch (Exception ex) { - Assert.Fail ("Did not expect an exception in Authenticate: {0}", ex); + Assert.ThrowsAsync (async () => await client.ConnectAsync (socket, null, port, SecureSocketOptions.Auto)); + Assert.ThrowsAsync (async () => await client.ConnectAsync (socket, "", port, SecureSocketOptions.Auto)); + Assert.ThrowsAsync (async () => await client.ConnectAsync (socket, host, -1, SecureSocketOptions.Auto)); + + await client.ConnectAsync (socket, host, port, SecureSocketOptions.Auto); + AssertGMailIsConnected (client); + Assert.That (connected, Is.EqualTo (1), "ConnectedEvent"); + + Assert.ThrowsAsync (async () => await client.ConnectAsync (socket, host, port, SecureSocketOptions.Auto)); + + await client.DisconnectAsync (true); + AssertClientIsDisconnected (client); + Assert.That (disconnected, Is.EqualTo (1), "DisconnectedEvent"); + } + } + + [Test] + public void TestConnectGmxDe () + { + var options = SecureSocketOptions.StartTls; + var host = "imap.gmx.de"; + int port = 143; + + using (var cancel = new CancellationTokenSource (30 * 1000)) { + using (var client = new ImapClient ()) { + int connected = 0, disconnected = 0; + + client.Connected += (sender, e) => { + Assert.That (e.Host, Is.EqualTo (host), "ConnectedEventArgs.Host"); + Assert.That (e.Port, Is.EqualTo (port), "ConnectedEventArgs.Port"); + Assert.That (e.Options, Is.EqualTo (options), "ConnectedEventArgs.Options"); + connected++; + }; + + client.Disconnected += (sender, e) => { + Assert.That (e.Host, Is.EqualTo (host), "DisconnectedEventArgs.Host"); + Assert.That (e.Port, Is.EqualTo (port), "DisconnectedEventArgs.Port"); + Assert.That (e.Options, Is.EqualTo (options), "DisconnectedEventArgs.Options"); + Assert.That (e.IsRequested, Is.True, "DisconnectedEventArgs.IsRequested"); + disconnected++; + }; + + var uri = new Uri ($"imap://{host}/?starttls=always"); + client.Connect (uri, cancel.Token); + Assert.That (client.IsConnected, Is.True, "Expected the client to be connected"); + Assert.That (client.IsSecure, Is.True, "Expected a secure connection"); + Assert.That (client.IsEncrypted, Is.True, "Expected an encrypted connection"); + Assert.That (client.IsSigned, Is.True, "Expected a signed connection"); + Assert.That (client.SslProtocol == SslProtocols.Tls12 || client.SslProtocol == SslProtocols.Tls13, Is.True, "Expected a TLS v1.2 or TLS v1.3 connection"); + Assert.That (client.SslCipherAlgorithm, Is.EqualTo (GmxDeCipherAlgorithm)); + Assert.That (client.SslCipherStrength, Is.EqualTo (GmxDeCipherStrength)); + Assert.That (client.SslHashAlgorithm, Is.EqualTo (GmxDeHashAlgorithm)); + Assert.That (client.SslHashStrength, Is.EqualTo (0), $"Unexpected SslHashStrength: {client.SslHashStrength}"); + Assert.That (client.SslKeyExchangeAlgorithm == ExchangeAlgorithmType.None || client.SslKeyExchangeAlgorithm == EcdhEphemeral, Is.True, $"Unexpected SslKeyExchangeAlgorithm: {client.SslKeyExchangeAlgorithm}"); + Assert.That (client.SslKeyExchangeStrength, Is.AnyOf (0, 255, 256, 384), $"Unexpected SslKeyExchangeStrength: {client.SslKeyExchangeStrength}"); + Assert.That (client.IsAuthenticated, Is.False, "Expected the client to not be authenticated"); + Assert.That (connected, Is.EqualTo (1), "ConnectedEvent"); + + client.Disconnect (true); + AssertClientIsDisconnected (client); + Assert.That (disconnected, Is.EqualTo (1), "DisconnectedEvent"); } + } + } - Assert.AreEqual (DovecotAuthenticatedCapabilities, client.Capabilities); - Assert.AreEqual (1, client.InternationalizationLevel, "Expected I18NLEVEL=1"); - Assert.IsTrue (client.ThreadingAlgorithms.Contains (ThreadingAlgorithm.OrderedSubject), "Expected THREAD=ORDEREDSUBJECT"); - Assert.IsTrue (client.ThreadingAlgorithms.Contains (ThreadingAlgorithm.References), "Expected THREAD=REFERENCES"); - // TODO: verify CONTEXT=SEARCH + [Test] + public async Task TestConnectGmxDeAsync () + { + var options = SecureSocketOptions.StartTls; + var host = "imap.gmx.de"; + int port = 143; + + using (var cancel = new CancellationTokenSource (30 * 1000)) { + using (var client = new ImapClient ()) { + int connected = 0, disconnected = 0; + + client.Connected += (sender, e) => { + Assert.That (e.Host, Is.EqualTo (host), "ConnectedEventArgs.Host"); + Assert.That (e.Port, Is.EqualTo (port), "ConnectedEventArgs.Port"); + Assert.That (e.Options, Is.EqualTo (options), "ConnectedEventArgs.Options"); + connected++; + }; + + client.Disconnected += (sender, e) => { + Assert.That (e.Host, Is.EqualTo (host), "DisconnectedEventArgs.Host"); + Assert.That (e.Port, Is.EqualTo (port), "DisconnectedEventArgs.Port"); + Assert.That (e.Options, Is.EqualTo (options), "DisconnectedEventArgs.Options"); + Assert.That (e.IsRequested, Is.True, "DisconnectedEventArgs.IsRequested"); + disconnected++; + }; + + var uri = new Uri ($"imap://{host}/?starttls=always"); + await client.ConnectAsync (uri, cancel.Token); + Assert.That (client.IsConnected, Is.True, "Expected the client to be connected"); + Assert.That (client.IsSecure, Is.True, "Expected a secure connection"); + Assert.That (client.IsEncrypted, Is.True, "Expected an encrypted connection"); + Assert.That (client.IsSigned, Is.True, "Expected a signed connection"); + Assert.That (client.SslProtocol == SslProtocols.Tls12 || client.SslProtocol == SslProtocols.Tls13, Is.True, "Expected a TLS v1.2 or TLS v1.3 connection"); + Assert.That (client.SslCipherAlgorithm, Is.EqualTo (GmxDeCipherAlgorithm)); + Assert.That (client.SslCipherStrength, Is.EqualTo (GmxDeCipherStrength)); + Assert.That (client.SslHashAlgorithm, Is.EqualTo (GmxDeHashAlgorithm)); + Assert.That (client.SslHashStrength, Is.EqualTo (0), $"Unexpected SslHashStrength: {client.SslHashStrength}"); + Assert.That (client.SslKeyExchangeAlgorithm == ExchangeAlgorithmType.None || client.SslKeyExchangeAlgorithm == EcdhEphemeral, Is.True, $"Unexpected SslKeyExchangeAlgorithm: {client.SslKeyExchangeAlgorithm}"); + Assert.That (client.SslKeyExchangeStrength, Is.AnyOf (0, 255, 256, 384), $"Unexpected SslKeyExchangeStrength: {client.SslKeyExchangeStrength}"); + Assert.That (client.IsAuthenticated, Is.False, "Expected the client to not be authenticated"); + Assert.That (connected, Is.EqualTo (1), "ConnectedEvent"); + + await client.DisconnectAsync (true); + AssertClientIsDisconnected (client); + Assert.That (disconnected, Is.EqualTo (1), "DisconnectedEvent"); + } + } + } - var personal = client.GetFolder (client.PersonalNamespaces[0]); + [Test] + public void TestConnectGmxDeSocket () + { + var options = SecureSocketOptions.StartTls; + var host = "imap.gmx.de"; + int port = 143; + + using (var cancel = new CancellationTokenSource (30 * 1000)) { + using (var client = new ImapClient ()) { + int connected = 0, disconnected = 0; + + client.Connected += (sender, e) => { + Assert.That (e.Host, Is.EqualTo (host), "ConnectedEventArgs.Host"); + Assert.That (e.Port, Is.EqualTo (port), "ConnectedEventArgs.Port"); + Assert.That (e.Options, Is.EqualTo (options), "ConnectedEventArgs.Options"); + connected++; + }; + + client.Disconnected += (sender, e) => { + Assert.That (e.Host, Is.EqualTo (host), "DisconnectedEventArgs.Host"); + Assert.That (e.Port, Is.EqualTo (port), "DisconnectedEventArgs.Port"); + Assert.That (e.Options, Is.EqualTo (options), "DisconnectedEventArgs.Options"); + Assert.That (e.IsRequested, Is.True, "DisconnectedEventArgs.IsRequested"); + disconnected++; + }; + + var socket = Connect (host, port); + client.Connect (socket, host, port, options, cancel.Token); + Assert.That (client.IsConnected, Is.True, "Expected the client to be connected"); + Assert.That (client.IsSecure, Is.True, "Expected a secure connection"); + Assert.That (client.IsEncrypted, Is.True, "Expected an encrypted connection"); + Assert.That (client.IsSigned, Is.True, "Expected a signed connection"); + Assert.That (client.SslProtocol == SslProtocols.Tls12 || client.SslProtocol == SslProtocols.Tls13, Is.True, "Expected a TLS v1.2 or TLS v1.3 connection"); + Assert.That (client.SslCipherAlgorithm, Is.EqualTo (GmxDeCipherAlgorithm)); + Assert.That (client.SslCipherStrength, Is.EqualTo (GmxDeCipherStrength)); + Assert.That (client.SslHashAlgorithm, Is.EqualTo (GmxDeHashAlgorithm)); + Assert.That (client.SslHashStrength, Is.EqualTo (0), $"Unexpected SslHashStrength: {client.SslHashStrength}"); + Assert.That (client.SslKeyExchangeAlgorithm == ExchangeAlgorithmType.None || client.SslKeyExchangeAlgorithm == EcdhEphemeral, Is.True, $"Unexpected SslKeyExchangeAlgorithm: {client.SslKeyExchangeAlgorithm}"); + Assert.That (client.SslKeyExchangeStrength, Is.AnyOf (0, 255, 256, 384), $"Unexpected SslKeyExchangeStrength: {client.SslKeyExchangeStrength}"); + Assert.That (client.IsAuthenticated, Is.False, "Expected the client to not be authenticated"); + Assert.That (connected, Is.EqualTo (1), "ConnectedEvent"); + + client.Disconnect (true); + AssertClientIsDisconnected (client); + Assert.That (disconnected, Is.EqualTo (1), "DisconnectedEvent"); + } + } + } - // Make sure these all throw NotSupportedException + [Test] + public async Task TestConnectGmxDeSocketAsync () + { + var options = SecureSocketOptions.StartTls; + var host = "imap.gmx.de"; + int port = 143; + + using (var cancel = new CancellationTokenSource (30 * 1000)) { + using (var client = new ImapClient ()) { + int connected = 0, disconnected = 0; + + client.Connected += (sender, e) => { + Assert.That (e.Host, Is.EqualTo (host), "ConnectedEventArgs.Host"); + Assert.That (e.Port, Is.EqualTo (port), "ConnectedEventArgs.Port"); + Assert.That (e.Options, Is.EqualTo (options), "ConnectedEventArgs.Options"); + connected++; + }; + + client.Disconnected += (sender, e) => { + Assert.That (e.Host, Is.EqualTo (host), "DisconnectedEventArgs.Host"); + Assert.That (e.Port, Is.EqualTo (port), "DisconnectedEventArgs.Port"); + Assert.That (e.Options, Is.EqualTo (options), "DisconnectedEventArgs.Options"); + Assert.That (e.IsRequested, Is.True, "DisconnectedEventArgs.IsRequested"); + disconnected++; + }; + + var socket = Connect (host, port); + await client.ConnectAsync (socket, host, port, options, cancel.Token); + Assert.That (client.IsConnected, Is.True, "Expected the client to be connected"); + Assert.That (client.IsSecure, Is.True, "Expected a secure connection"); + Assert.That (client.IsEncrypted, Is.True, "Expected an encrypted connection"); + Assert.That (client.IsSigned, Is.True, "Expected a signed connection"); + Assert.That (client.SslProtocol == SslProtocols.Tls12 || client.SslProtocol == SslProtocols.Tls13, Is.True, "Expected a TLS v1.2 or TLS v1.3 connection"); + Assert.That (client.SslCipherAlgorithm, Is.EqualTo (GmxDeCipherAlgorithm)); + Assert.That (client.SslCipherStrength, Is.EqualTo (GmxDeCipherStrength)); + Assert.That (client.SslHashAlgorithm, Is.EqualTo (GmxDeHashAlgorithm)); + Assert.That (client.SslHashStrength, Is.EqualTo (0), $"Unexpected SslHashStrength: {client.SslHashStrength}"); + Assert.That (client.SslKeyExchangeAlgorithm == ExchangeAlgorithmType.None || client.SslKeyExchangeAlgorithm == EcdhEphemeral, Is.True, $"Unexpected SslKeyExchangeAlgorithm: {client.SslKeyExchangeAlgorithm}"); + Assert.That (client.SslKeyExchangeStrength, Is.AnyOf (0, 255, 256, 384), $"Unexpected SslKeyExchangeStrength: {client.SslKeyExchangeStrength}"); + Assert.That (client.IsAuthenticated, Is.False, "Expected the client to not be authenticated"); + Assert.That (connected, Is.EqualTo (1), "ConnectedEvent"); + + await client.DisconnectAsync (true); + AssertClientIsDisconnected (client); + Assert.That (disconnected, Is.EqualTo (1), "DisconnectedEvent"); + } + } + } + + [Test] + public void TestUnexpectedGreeting () + { + var commands = new List { + new ImapReplayCommand ("", Encoding.ASCII.GetBytes ("* INVALID\r\n")) + }; + + using (var client = new ImapClient () { TagPrefix = 'A' }) + Assert.Throws (() => client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None), "Connect"); + + using (var client = new ImapClient () { TagPrefix = 'A' }) + Assert.ThrowsAsync (() => client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None), "ConnectAsync"); + } + + [Test] + public void TestGreetingCapabilities () + { + var commands = new List { + new ImapReplayCommand ("", "common.capability-greeting.txt") + }; + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + + Assert.That (client.Capabilities, Is.EqualTo (GreetingCapabilities)); + Assert.That (client.AuthenticationMechanisms, Has.Count.EqualTo (1)); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN"), "Expected SASL PLAIN auth mechanism"); + } + } + + [Test] + public async Task TestGreetingCapabilitiesAsync () + { + var commands = new List { + new ImapReplayCommand ("", "common.capability-greeting.txt") + }; + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + + Assert.That (client.Capabilities, Is.EqualTo (GreetingCapabilities)); + Assert.That (client.AuthenticationMechanisms, Has.Count.EqualTo (1)); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN"), "Expected SASL PLAIN auth mechanism"); + } + } + + [Test] + public void TestByeGreeting () + { + var commands = new List { + new ImapReplayCommand ("", Encoding.ASCII.GetBytes ("* BYE\r\n")) + }; + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + Assert.Fail ("Did not expect to be connected."); + } catch (ImapProtocolException ex) { + Assert.That (ex.Message, Is.EqualTo ("The IMAP server unexpectedly refused the connection.")); + } catch (Exception ex) { + Assert.Fail ($"Did not expect this exception in Connect: {ex}"); + } + + client.Disconnect (false); + } + } + + [Test] + public async Task TestByeGreetingAsync () + { + var commands = new List { + new ImapReplayCommand ("", Encoding.ASCII.GetBytes ("* BYE\r\n")) + }; + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + Assert.Fail ("Did not expect to be connected."); + } catch (ImapProtocolException ex) { + Assert.That (ex.Message, Is.EqualTo ("The IMAP server unexpectedly refused the connection.")); + } catch (Exception ex) { + Assert.Fail ($"Did not expect this exception in Connect: {ex}"); + } + + await client.DisconnectAsync (false); + } + } + + [Test] + public void TestByeGreetingWithAlert () + { + var commands = new List { + new ImapReplayCommand ("", Encoding.ASCII.GetBytes ("* BYE [ALERT] Too many connections.\r\n")) + }; + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + int alerts = 0; + + client.Alert += (sender, e) => { + Assert.That (e.Message, Is.EqualTo ("Too many connections.")); + alerts++; + }; + + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + Assert.Fail ("Did not expect to be connected."); + } catch (ImapProtocolException ex) { + Assert.That (ex.Message, Is.EqualTo ("Too many connections.")); + } catch (Exception ex) { + Assert.Fail ($"Did not expect this exception in Connect: {ex}"); + } + + Assert.That (alerts, Is.EqualTo (1), "Expected 1 alert"); + + client.Disconnect (false); + } + } + + [Test] + public async Task TestByeGreetingWithAlertAsync () + { + var commands = new List { + new ImapReplayCommand ("", Encoding.ASCII.GetBytes ("* BYE [ALERT] Too many connections.\r\n")) + }; + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + int alerts = 0; + + client.Alert += (sender, e) => { + Assert.That (e.Message, Is.EqualTo ("Too many connections.")); + alerts++; + }; + + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + Assert.Fail ("Did not expect to be connected."); + } catch (ImapProtocolException ex) { + Assert.That (ex.Message, Is.EqualTo ("Too many connections.")); + } catch (Exception ex) { + Assert.Fail ($"Did not expect this exception in Connect: {ex}"); + } + + Assert.That (alerts, Is.EqualTo (1), "Expected 1 alert"); + + await client.DisconnectAsync (false); + } + } + + [Test] + public void TestByeGreetingWithRespText () + { + var commands = new List { + new ImapReplayCommand ("", Encoding.ASCII.GetBytes ("* BYE Too many connections.\r\n")) + }; + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + Assert.Fail ("Did not expect to be connected."); + } catch (ImapProtocolException ex) { + Assert.That (ex.Message, Is.EqualTo ("Too many connections.")); + } catch (Exception ex) { + Assert.Fail ($"Did not expect this exception in Connect: {ex}"); + } + + client.Disconnect (false); + } + } + + [Test] + public async Task TestByeGreetingWithRespTextAsync () + { + var commands = new List { + new ImapReplayCommand ("", Encoding.ASCII.GetBytes ("* BYE Too many connections.\r\n")) + }; + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + Assert.Fail ("Did not expect to be connected."); + } catch (ImapProtocolException ex) { + Assert.That (ex.Message, Is.EqualTo ("Too many connections.")); + } catch (Exception ex) { + Assert.Fail ($"Did not expect this exception in Connect: {ex}"); + } + + await client.DisconnectAsync (false); + } + } + + static List CreateUnexpectedByeCommands () + { + return new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt"), + new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "gmail.authenticate+statussize+objectid.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-inbox.txt"), + new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt"), + new ImapReplayCommand ("A00000005 SELECT INBOX (CONDSTORE)\r\n", Encoding.ASCII.GetBytes ("* BYE System going down for a reboot.\r\n")) + }; + } + + [Test] + public void TestUnexpectedBye () + { + var commands = CreateUnexpectedByeCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect this exception in Connect: {ex}"); + } + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + try { + client.Inbox.Open (FolderAccess.ReadWrite); + Assert.Fail ("Did not expect to open the Inbox"); + } catch (ImapProtocolException ex) { + Assert.That (ex.Message, Is.EqualTo ("System going down for a reboot.")); + } catch (Exception ex) { + Assert.Fail ($"Did not expect this exception in Open: {ex}"); + } + + client.Disconnect (false); + } + } + + [Test] + public async Task TestUnexpectedByeAsync () + { + var commands = CreateUnexpectedByeCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect this exception in Connect: {ex}"); + } + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + try { + await client.Inbox.OpenAsync (FolderAccess.ReadWrite); + Assert.Fail ("Did not expect to open the Inbox"); + } catch (ImapProtocolException ex) { + Assert.That (ex.Message, Is.EqualTo ("System going down for a reboot.")); + } catch (Exception ex) { + Assert.Fail ($"Did not expect this exception in Open: {ex}"); + } + + await client.DisconnectAsync (false); + } + } + + static List CreateUnexpectedByeAfterCapabilityCommands () + { + return new List { + new ImapReplayCommand ("", Encoding.ASCII.GetBytes ("* OK Yandex IMAP4rev1 at sas8-bccc92f57f23.qloud-c.yandex.net:993 ready to talk with, 2019-Oct-18 07:41:00, 0fHtH613ZiE1\r\n")), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", Encoding.ASCII.GetBytes ("* BYE Autologout; idle for too long (1)\r\n* BYE Autologout; idle for too long (2)\r\n* BYE Autologout; idle for too long (3)\r\n")) + }; + } + + [Test] + public void TestUnexpectedByeAfterCapability () + { + var commands = CreateUnexpectedByeAfterCapabilityCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + Assert.Fail ("Did not expect to connect"); + } catch (ImapProtocolException ex) { + Assert.That (ex.Message, Is.EqualTo ("Autologout; idle for too long (1)")); + } catch (Exception ex) { + Assert.Fail ($"Did not expect this exception in Connect: {ex}"); + } + + client.Disconnect (false); + } + } + + [Test] + public async Task TestUnexpectedByeAfterCapabilityAsync () + { + var commands = CreateUnexpectedByeAfterCapabilityCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + Assert.Fail ("Did not expect to connect"); + } catch (ImapProtocolException ex) { + Assert.That (ex.Message, Is.EqualTo ("Autologout; idle for too long (1)")); + } catch (Exception ex) { + Assert.Fail ($"Did not expect this exception in Connect: {ex}"); + } + + await client.DisconnectAsync (false); + } + } + + static List CreateUnexpectedByeWithAlertCommands () + { + return new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt"), + new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "gmail.authenticate+statussize+objectid.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-inbox.txt"), + new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt"), + new ImapReplayCommand ("A00000005 SELECT INBOX (CONDSTORE)\r\n", Encoding.ASCII.GetBytes ("* BYE [ALERT] System going down for a reboot.\r\n")) + }; + } + + [Test] + public void TestUnexpectedByeWithAlert () + { + var commands = CreateUnexpectedByeWithAlertCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + int alerts = 0; + + client.Alert += (sender, e) => { + Assert.That (e.Message, Is.EqualTo ("System going down for a reboot.")); + alerts++; + }; + + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect this exception in Connect: {ex}"); + } + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + try { + client.Inbox.Open (FolderAccess.ReadWrite); + Assert.Fail ("Did not expect to open the Inbox"); + } catch (ImapProtocolException ex) { + Assert.That (ex.Message, Is.EqualTo ("System going down for a reboot.")); + } catch (Exception ex) { + Assert.Fail ($"Did not expect this exception in Open: {ex}"); + } + + Assert.That (alerts, Is.EqualTo (1), "Expected 1 alert"); + + client.Disconnect (false); + } + } + + [Test] + public async Task TestUnexpectedByeWithAlertAsync () + { + var commands = CreateUnexpectedByeWithAlertCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + int alerts = 0; + + client.Alert += (sender, e) => { + Assert.That (e.Message, Is.EqualTo ("System going down for a reboot.")); + alerts++; + }; + + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect this exception in Connect: {ex}"); + } + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + try { + await client.Inbox.OpenAsync (FolderAccess.ReadWrite); + Assert.Fail ("Did not expect to open the Inbox"); + } catch (ImapProtocolException ex) { + Assert.That (ex.Message, Is.EqualTo ("System going down for a reboot.")); + } catch (Exception ex) { + Assert.Fail ($"Did not expect this exception in Open: {ex}"); + } + + Assert.That (alerts, Is.EqualTo (1), "Expected 1 alert"); + + await client.DisconnectAsync (false); + } + } + + static List CreateUnexpectedByeInSaslAuthenticateCommands () + { + return new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt"), + new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", Encoding.ASCII.GetBytes ("* BYE disconnecting\r\nA00000001 NO you are not allowed to act as a proxy server\r\n")) + }; + } + + [Test] + public void TestUnexpectedByeInSaslAuthenticate () + { + var commands = CreateUnexpectedByeInSaslAuthenticateCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + + try { + client.Authenticate ("username", "password"); + Assert.Fail ("Expected failure"); + } catch (ImapProtocolException pex) { + Assert.That (pex.Message, Is.EqualTo ("you are not allowed to act as a proxy server")); + } catch (Exception ex) { + Assert.Fail ($"Expected ImapProtocolException, but got: {ex}"); + } + } + } + + [Test] + public async Task TestUnexpectedByeInSaslAuthenticateAsync () + { + var commands = CreateUnexpectedByeInSaslAuthenticateCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + + try { + await client.AuthenticateAsync ("username", "password"); + Assert.Fail ("Expected failure"); + } catch (ImapProtocolException pex) { + Assert.That (pex.Message, Is.EqualTo ("you are not allowed to act as a proxy server")); + } catch (Exception ex) { + Assert.Fail ($"Expected ImapProtocolException, but got: {ex}"); + } + } + } + + static List CreateInvalidTaggedByeDuringLogoutCommands () + { + return new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt"), + new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "gmail.authenticate+statussize+objectid.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-inbox.txt"), + new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt"), + new ImapReplayCommand ("A00000005 LOGOUT\r\n", Encoding.ASCII.GetBytes ("A00000005 BYE IMAP4rev1 Server logging out\r\n")) + }; + } + + [Test] + public void TestInvalidTaggedByeDuringLogout () + { + var commands = CreateInvalidTaggedByeDuringLogoutCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect this exception in Connect: {ex}"); + } + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + try { + client.Disconnect (true); + } catch (Exception ex) { + Assert.Fail ($"Exceptions should be swallowed in Disconnect: {ex}"); + } + } + } + + [Test] + public async Task TestInvalidTaggedByeDuringLogoutAsync () + { + var commands = CreateInvalidTaggedByeDuringLogoutCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect this exception in Connect: {ex}"); + } + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + try { + await client.DisconnectAsync (true); + } catch (Exception ex) { + Assert.Fail ($"Exceptions should be swallowed in Disconnect: {ex}"); + } + } + } + + static List CreatePreAuthGreetingCommands () + { + return new List { + new ImapReplayCommand ("", "common.preauth-greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "common.capability.txt"), + new ImapReplayCommand ("A00000001 LIST \"\" \"\"\r\n", "common.list-namespace.txt"), + new ImapReplayCommand ("A00000002 LIST \"\" \"INBOX\"\r\n", "common.list-inbox.txt") + }; + } + + [Test] + public void TestPreAuthGreeting () + { + var capabilities = ImapCapabilities.IMAP4rev1 | ImapCapabilities.Status; + var commands = CreatePreAuthGreetingCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + int authenticated = 0; + client.Authenticated += (sender, e) => { + authenticated++; + }; + + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + Assert.That (client.IsAuthenticated, Is.True, "Client should be authenticated."); + Assert.That (authenticated, Is.EqualTo (1), "Authenticated event was not emitted the expected number of times"); + Assert.That (client.IsSecure, Is.False, "IsSecure should be false."); + + Assert.That (client.Capabilities, Is.EqualTo (capabilities), "Capabilities"); + + var personal = client.GetFolder (client.PersonalNamespaces[0]); + var inbox = client.Inbox; + + Assert.That (inbox, Is.Not.Null, "Expected non-null Inbox folder."); + Assert.That (inbox.Attributes, Is.EqualTo (FolderAttributes.Inbox), "Expected Inbox attributes to be empty."); + + client.Disconnect (false); + } + } + + [Test] + public async Task TestPreAuthGreetingAsync () + { + var capabilities = ImapCapabilities.IMAP4rev1 | ImapCapabilities.Status; + var commands = CreatePreAuthGreetingCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + int authenticated = 0; + client.Authenticated += (sender, e) => { + authenticated++; + }; + + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + Assert.That (client.IsAuthenticated, Is.True, "Client should be authenticated."); + Assert.That (authenticated, Is.EqualTo (1), "Authenticated event was not emitted the expected number of times"); + Assert.That (client.IsSecure, Is.False, "IsSecure should be false."); + + Assert.That (client.Capabilities, Is.EqualTo (capabilities), "Capabilities"); + + var personal = client.GetFolder (client.PersonalNamespaces[0]); + var inbox = client.Inbox; + + Assert.That (inbox, Is.Not.Null, "Expected non-null Inbox folder."); + Assert.That (inbox.Attributes, Is.EqualTo (FolderAttributes.Inbox), "Expected Inbox attributes to be empty."); + + await client.DisconnectAsync (false); + } + } + + static List CreatePreAuthCapabilityGreetingCommands () + { + return new List { + new ImapReplayCommand ("", "common.preauth-capability-greeting.txt"), + new ImapReplayCommand ("A00000000 LIST \"\" \"\"\r\n", "common.list-namespace.txt"), + new ImapReplayCommand ("A00000001 LIST \"\" \"INBOX\"\r\n", "common.list-inbox.txt") + }; + } + + [Test] + public void TestPreAuthCapabilityGreeting () + { + var capabilities = ImapCapabilities.IMAP4rev1 | ImapCapabilities.Status; + var commands = CreatePreAuthCapabilityGreetingCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + int authenticated = 0; + client.Authenticated += (sender, e) => { + authenticated++; + }; + + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + Assert.That (client.IsAuthenticated, Is.True, "Client should be authenticated."); + Assert.That (authenticated, Is.EqualTo (1), "Authenticated event was not emitted the expected number of times"); + Assert.That (client.IsSecure, Is.False, "IsSecure should be false."); + + Assert.That (client.Capabilities, Is.EqualTo (capabilities), "Capabilities"); + + var personal = client.GetFolder (client.PersonalNamespaces[0]); + var inbox = client.Inbox; + + Assert.That (inbox, Is.Not.Null, "Expected non-null Inbox folder."); + Assert.That (inbox.Attributes, Is.EqualTo (FolderAttributes.Inbox), "Expected Inbox attributes to be empty."); + + client.Disconnect (false); + } + } + + [Test] + public async Task TestPreAuthCapabilityGreetingAsync () + { + var capabilities = ImapCapabilities.IMAP4rev1 | ImapCapabilities.Status; + var commands = CreatePreAuthCapabilityGreetingCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + int authenticated = 0; + client.Authenticated += (sender, e) => { + authenticated++; + }; + + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + Assert.That (client.IsAuthenticated, Is.True, "Client should be authenticated."); + Assert.That (authenticated, Is.EqualTo (1), "Authenticated event was not emitted the expected number of times"); + Assert.That (client.IsSecure, Is.False, "IsSecure should be false."); + + Assert.That (client.Capabilities, Is.EqualTo (capabilities), "Capabilities"); + + var personal = client.GetFolder (client.PersonalNamespaces[0]); + var inbox = client.Inbox; + + Assert.That (inbox, Is.Not.Null, "Expected non-null Inbox folder."); + Assert.That (inbox.Attributes, Is.EqualTo (FolderAttributes.Inbox), "Expected Inbox attributes to be empty."); + + await client.DisconnectAsync (false); + } + } + + static List CreateGMailWebAlertCommands () + { + return new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt"), + new ImapReplayCommand ("A00000001 LOGIN username password\r\n", "gmail.authenticate+webalert.txt") + }; + } + + [Test] + public void TestGMailWebAlert () + { + const string webUri = "https://accounts.google.com/signin/continue?sarp=1&scc=1&plt=AKgnsbsNd6RU3LIlgDfhmL9Y7ywYhtagFig_xfuSJCUHD9Eg3XqN8DKlDk3G8jmj2w5viIm5PDC3BS4SVy7iFMB6g1244cnQt1E60EdOTSEpnqDzL6FH2L-ReOAyZ3qkSXZQZs2pIfL2"; + const string alert = "Please log in via your web browser: https://support.google.com/mail/accounts/answer/78754 (Failure)"; + + var commands = CreateGMailWebAlertCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + int webalerts = 0; + int alerts = 0; + + client.Alert += (sender, e) => { + Assert.That (e.Message, Is.EqualTo (alert)); + alerts++; + }; + + client.WebAlert += (sender, e) => { + Assert.That (e.WebUri.AbsoluteUri, Is.EqualTo (webUri)); + Assert.That (e.Message, Is.EqualTo ("Web login required.")); + webalerts++; + }; + + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + client.AuthenticationMechanisms.Clear (); + + try { + client.Authenticate ("username", "password"); + Assert.Fail ("Did not expect Authenticate to work."); + } catch (AuthenticationException ax) { + Assert.That (ax.Message, Is.EqualTo (alert)); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (alerts, Is.EqualTo (1), "Expected 1 alert"); + Assert.That (webalerts, Is.EqualTo (1), "Expected 1 web alert"); + + client.Disconnect (false); + } + } + + [Test] + public async Task TestGMailWebAlertAsync () + { + const string webUri = "https://accounts.google.com/signin/continue?sarp=1&scc=1&plt=AKgnsbsNd6RU3LIlgDfhmL9Y7ywYhtagFig_xfuSJCUHD9Eg3XqN8DKlDk3G8jmj2w5viIm5PDC3BS4SVy7iFMB6g1244cnQt1E60EdOTSEpnqDzL6FH2L-ReOAyZ3qkSXZQZs2pIfL2"; + const string alert = "Please log in via your web browser: https://support.google.com/mail/accounts/answer/78754 (Failure)"; + + var commands = CreateGMailWebAlertCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + int webalerts = 0; + int alerts = 0; + + client.Alert += (sender, e) => { + Assert.That (e.Message, Is.EqualTo (alert)); + alerts++; + }; + + client.WebAlert += (sender, e) => { + Assert.That (e.WebUri.AbsoluteUri, Is.EqualTo (webUri)); + Assert.That (e.Message, Is.EqualTo ("Web login required.")); + webalerts++; + }; + + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + client.AuthenticationMechanisms.Clear (); + + try { + await client.AuthenticateAsync ("username", "password"); + Assert.Fail ("Did not expect Authenticate to work."); + } catch (AuthenticationException ax) { + Assert.That (ax.Message, Is.EqualTo (alert)); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (alerts, Is.EqualTo (1), "Expected 1 alert"); + Assert.That (webalerts, Is.EqualTo (1), "Expected 1 web alert"); + + await client.DisconnectAsync (false); + } + } + + static List CreateUnicodeRespTextCommands (out string respText) + { + respText = "╟ы╩╣╙├╩┌╚и┬ы╡╟┬╝бг╧ъ╟щ╟ы┐┤"; + + return new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt"), + new ImapReplayCommand ("A00000001 LOGIN username password\r\n", Encoding.UTF8.GetBytes ("A00000001 NO " + respText + "\r\n")) + }; + } + + [Test] + public void TestUnicodeRespText () + { + var commands = CreateUnicodeRespTextCommands (out var respText); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + client.AuthenticationMechanisms.Clear (); + + try { + client.Authenticate ("username", "password"); + Assert.Fail ("Did not expect Authenticate to work."); + } catch (AuthenticationException ax) { + Assert.That (ax.Message, Is.EqualTo (respText)); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + client.Disconnect (false); + } + } + + [Test] + public async Task TestUnicodeRespTextAsync () + { + var commands = CreateUnicodeRespTextCommands (out var respText); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + client.AuthenticationMechanisms.Clear (); + + try { + await client.AuthenticateAsync ("username", "password"); + Assert.Fail ("Did not expect Authenticate to work."); + } catch (AuthenticationException ax) { + Assert.That (ax.Message, Is.EqualTo (respText)); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + await client.DisconnectAsync (false); + } + } + + static List CreateInvalidUntaggedResponseCommands () + { + return new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt"), + new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "gmail.authenticate.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-inbox.txt"), + new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt"), + new ImapReplayCommand ("A00000005 LIST \"\" \"Buggy Folder Listing\" RETURN (SUBSCRIBED CHILDREN)\r\n", Encoding.ASCII.GetBytes ("* {25}\r\nThis should be skipped...\r\n* LIST (\\NoSelect) \"/\" \"Buggy Folder Listing\"\r\nA00000005 OK LIST completed.\r\n")), + }; + } + + [Test] + public void TestInvalidUntaggedResponse () + { + var commands = CreateInvalidUntaggedResponseCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + var folder = client.GetFolder ("Buggy Folder Listing"); + Assert.That (folder.Name, Is.EqualTo ("Buggy Folder Listing"), "Name"); + Assert.That (folder.Attributes, Is.EqualTo (FolderAttributes.NoSelect), "Attributes"); + + client.Disconnect (false); + } + } + + [Test] + public async Task TestInvalidUntaggedResponseAsync () + { + var commands = CreateInvalidUntaggedResponseCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + var folder = await client.GetFolderAsync ("Buggy Folder Listing"); + Assert.That (folder.Name, Is.EqualTo ("Buggy Folder Listing"), "Name"); + Assert.That (folder.Attributes, Is.EqualTo (FolderAttributes.NoSelect), "Attributes"); + + await client.DisconnectAsync (false); + } + } + + static List CreateInvalidUntaggedBadResponseCommands (out string alertText) + { + alertText = "Please enable IMAP access in your account settings first."; + + return new List { + new ImapReplayCommand ("", "common.capability-greeting.txt"), + new ImapReplayCommand ("A00000000 LOGIN username password\r\n", Encoding.UTF8.GetBytes ("A00000000 OK [ALERT] " + alertText + "\r\n")), + new ImapReplayCommand ("A00000001 CAPABILITY\r\n", Encoding.UTF8.GetBytes ("A00000001 NO [ALERT] " + alertText + "\r\n")), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", Encoding.UTF8.GetBytes ("A00000002 NO [ALERT] " + alertText + "\r\n")), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\"\r\n", Encoding.UTF8.GetBytes ("* BAD [ALERT] " + alertText + "\r\nA00000003 NO [ALERT] " + alertText + "\r\n")) + }; + } + + [Test] + public void TestInvalidUntaggedBadResponse () + { + var commands = CreateInvalidUntaggedBadResponseCommands (out var alertText); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + client.AuthenticationMechanisms.Clear (); + + int alerts = 0; + client.Alert += (sender, e) => { + Assert.That (e.Message, Is.EqualTo (alertText)); + alerts++; + }; + + try { + client.Authenticate ("username", "password"); + } catch (AuthenticationException ax) { + Assert.That (ax.Message, Is.EqualTo (alertText)); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (alerts, Is.EqualTo (5), $"Unexpected number of alerts: {alerts}"); + + Assert.That (client.Inbox, Is.Not.Null, "Inbox"); + + client.Disconnect (false); + } + } + + [Test] + public async Task TestInvalidUntaggedBadResponseAsync () + { + var commands = CreateInvalidUntaggedBadResponseCommands (out var alertText); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + client.AuthenticationMechanisms.Clear (); + + int alerts = 0; + client.Alert += (sender, e) => { + Assert.That (e.Message, Is.EqualTo (alertText)); + alerts++; + }; + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (AuthenticationException ax) { + Assert.That (ax.Message, Is.EqualTo (alertText)); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (alerts, Is.EqualTo (5), $"Unexpected number of alerts: {alerts}"); + + Assert.That (client.Inbox, Is.Not.Null, "Inbox"); + + await client.DisconnectAsync (false); + } + } + + // Tests issue https://github.com/jstedfast/MailKit/issues/115#issuecomment-313684616 + static IList CreateUntaggedRespCodeCommands () + { + return new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt"), + new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "gmail.authenticate.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-inbox.txt"), + new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt"), + new ImapReplayCommand ("A00000005 SELECT INBOX (CONDSTORE)\r\n", "gmail.select-inbox.txt"), + new ImapReplayCommand ("A00000006 UID MOVE 1 \"[Gmail]/Trash\"\r\n", Encoding.ASCII.GetBytes ("* [COPYUID 123456 1 2]\r\n* 1 EXPUNGE\r\nA00000006 OK Completed.\r\n")) + }; + } + + [Test] + public void TestUntaggedRespCode () + { + var commands = CreateUntaggedRespCodeCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + var trash = client.GetFolder (SpecialFolder.Trash); + var inbox = client.Inbox; + + inbox.Open (FolderAccess.ReadWrite); + var moved = inbox.MoveTo (UniqueId.MinValue, trash); + Assert.That (moved.Value.Id, Is.EqualTo (2)); + + client.Disconnect (false); + } + } + + [Test] + public async Task TestUntaggedRespCodeAsync () + { + var commands = CreateUntaggedRespCodeCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + var trash = client.GetFolder (SpecialFolder.Trash); + var inbox = client.Inbox; + + await inbox.OpenAsync (FolderAccess.ReadWrite); + var moved = await inbox.MoveToAsync (UniqueId.MinValue, trash); + Assert.That (moved.Value.Id, Is.EqualTo (2)); + + await client.DisconnectAsync (false); + } + } + + static IList CreateSuperfluousUntaggedOkNoOrBadCommands () + { + return new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt"), + new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "gmail.authenticate.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-inbox.txt"), + new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt"), + new ImapReplayCommand ("A00000005 SELECT INBOX (CONDSTORE)\r\n", "gmail.select-inbox.txt"), + new ImapReplayCommand ("A00000006 UID MOVE 1 \"[Gmail]/Trash\"\r\n", Encoding.ASCII.GetBytes ("* OK The good,\r\n* BAD the bad,\r\n* NO and the ugly.\r\n* OK [COPYUID 123456 1 2]\r\n* 1 EXPUNGE\r\nA00000006 OK Completed.\r\n")) + }; + } + + [Test] + public void TesSuperfluousUntaggedOkNoOrBad () + { + var commands = CreateSuperfluousUntaggedOkNoOrBadCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + var trash = client.GetFolder (SpecialFolder.Trash); + var inbox = client.Inbox; + + inbox.Open (FolderAccess.ReadWrite); + var moved = inbox.MoveTo (UniqueId.MinValue, trash); + Assert.That (moved.Value.Id, Is.EqualTo (2)); + + client.Disconnect (false); + } + } + + [Test] + public async Task TestSuperfluousUntaggedOkNoOrBadAsync () + { + var commands = CreateSuperfluousUntaggedOkNoOrBadCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + var trash = client.GetFolder (SpecialFolder.Trash); + var inbox = client.Inbox; + + await inbox.OpenAsync (FolderAccess.ReadWrite); + var moved = await inbox.MoveToAsync (UniqueId.MinValue, trash); + Assert.That (moved.Value.Id, Is.EqualTo (2)); + + await client.DisconnectAsync (false); + } + } + + static List CreateLoginCommands () + { + return new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt"), + new ImapReplayCommand ("A00000001 LOGIN \"Indiana \\\"Han Solo\\\" Jones\" \"p@ss\\\\word\"\r\n", "gmail.authenticate.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-inbox.txt"), + new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt") + }; + } + + [Test] + public void TestLogin () + { + var commands = CreateLoginCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + Assert.That (client.IsSecure, Is.False, "IsSecure should be false."); + + Assert.That (client.Capabilities, Is.EqualTo (GMailInitialCapabilities)); + Assert.That (client.AuthenticationMechanisms, Has.Count.EqualTo (5)); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH"), "Expected SASL XOAUTH auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH2"), "Expected SASL XOAUTH2 auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("OAUTHBEARER"), "Expected SASL OAUTHBEARER auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN"), "Expected SASL PLAIN auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN-CLIENTTOKEN"), "Expected SASL PLAIN-CLIENTTOKEN auth mechanism"); + + Assert.That (client.Timeout, Is.EqualTo (120000), "Timeout"); + client.Timeout *= 2; + + // Note: Do not try to use any SASL mechanisms + client.AuthenticationMechanisms.Clear (); + + int authenticated = 0; + client.Authenticated += (sender, e) => { + authenticated++; + }; + + try { + client.Authenticate (new NetworkCredential ("Indiana \"Han Solo\" Jones", "p@ss\\word")); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (authenticated, Is.EqualTo (1), "Authenticated event was not emitted the expected number of times"); + Assert.That (client.Capabilities, Is.EqualTo (GMailAuthenticatedCapabilities)); + Assert.That (client.SupportsQuotas, Is.True, "SupportsQuotas"); + + client.Disconnect (false); + } + } + + [Test] + public async Task TestLoginAsync () + { + var commands = CreateLoginCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + Assert.That (client.IsSecure, Is.False, "IsSecure should be false."); + + Assert.That (client.Capabilities, Is.EqualTo (GMailInitialCapabilities)); + Assert.That (client.AuthenticationMechanisms, Has.Count.EqualTo (5)); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH"), "Expected SASL XOAUTH auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH2"), "Expected SASL XOAUTH2 auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("OAUTHBEARER"), "Expected SASL OAUTHBEARER auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN"), "Expected SASL PLAIN auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN-CLIENTTOKEN"), "Expected SASL PLAIN-CLIENTTOKEN auth mechanism"); + + Assert.That (client.Timeout, Is.EqualTo (120000), "Timeout"); + client.Timeout *= 2; + + // Note: Do not try to use any SASL mechanisms + client.AuthenticationMechanisms.Clear (); + + int authenticated = 0; + client.Authenticated += (sender, e) => { + authenticated++; + }; + + try { + await client.AuthenticateAsync (new NetworkCredential ("Indiana \"Han Solo\" Jones", "p@ss\\word")); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (authenticated, Is.EqualTo (1), "Authenticated event was not emitted the expected number of times"); + Assert.That (client.Capabilities, Is.EqualTo (GMailAuthenticatedCapabilities)); + Assert.That (client.SupportsQuotas, Is.True, "SupportsQuotas"); + + await client.DisconnectAsync (false); + } + } + + static List CreateLoginSpecialCharacterCommands () + { + return new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt"), + new ImapReplayCommand ("A00000001 LOGIN username \"pass%word\"\r\n", "gmail.authenticate.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-inbox.txt"), + new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt") + }; + } + + [Test] + public void TestLoginSpecialCharacter () + { + var commands = CreateLoginSpecialCharacterCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + Assert.That (client.IsSecure, Is.False, "IsSecure should be false."); + + Assert.That (client.Capabilities, Is.EqualTo (GMailInitialCapabilities)); + Assert.That (client.AuthenticationMechanisms, Has.Count.EqualTo (5)); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH"), "Expected SASL XOAUTH auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH2"), "Expected SASL XOAUTH2 auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("OAUTHBEARER"), "Expected SASL OAUTHBEARER auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN"), "Expected SASL PLAIN auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN-CLIENTTOKEN"), "Expected SASL PLAIN-CLIENTTOKEN auth mechanism"); + + Assert.That (client.Timeout, Is.EqualTo (120000), "Timeout"); + client.Timeout *= 2; + + // Note: Do not try to use any SASL mechanisms + client.AuthenticationMechanisms.Clear (); + + int authenticated = 0; + client.Authenticated += (sender, e) => { + authenticated++; + }; + + try { + client.Authenticate ("username", "pass%word"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (authenticated, Is.EqualTo (1), "Authenticated event was not emitted the expected number of times"); + Assert.That (client.Capabilities, Is.EqualTo (GMailAuthenticatedCapabilities)); + Assert.That (client.SupportsQuotas, Is.True, "SupportsQuotas"); + + client.Disconnect (false); + } + } + + [Test] + public async Task TestLoginSpecialCharacterAsync () + { + var commands = CreateLoginSpecialCharacterCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + Assert.That (client.IsSecure, Is.False, "IsSecure should be false."); + + Assert.That (client.Capabilities, Is.EqualTo (GMailInitialCapabilities)); + Assert.That (client.AuthenticationMechanisms, Has.Count.EqualTo (5)); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH"), "Expected SASL XOAUTH auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH2"), "Expected SASL XOAUTH2 auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("OAUTHBEARER"), "Expected SASL OAUTHBEARER auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN"), "Expected SASL PLAIN auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN-CLIENTTOKEN"), "Expected SASL PLAIN-CLIENTTOKEN auth mechanism"); + + Assert.That (client.Timeout, Is.EqualTo (120000), "Timeout"); + client.Timeout *= 2; + + // Note: Do not try to use any SASL mechanisms + client.AuthenticationMechanisms.Clear (); + + int authenticated = 0; + client.Authenticated += (sender, e) => { + authenticated++; + }; + + try { + await client.AuthenticateAsync ("username", "pass%word"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (authenticated, Is.EqualTo (1), "Authenticated event was not emitted the expected number of times"); + Assert.That (client.Capabilities, Is.EqualTo (GMailAuthenticatedCapabilities)); + Assert.That (client.SupportsQuotas, Is.True, "SupportsQuotas"); + + await client.DisconnectAsync (false); + } + } + + static List CreateLoginDisabledCommands () + { + return new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability+logindisabled.txt"), + new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", ImapReplayCommandResponse.NO) + }; + } + + [Test] + public void TestLoginDisabled () + { + var commands = CreateLoginDisabledCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + Assert.That (client.IsSecure, Is.False, "IsSecure should be false."); + + Assert.That (client.Capabilities, Is.EqualTo (GMailInitialCapabilities | ImapCapabilities.LoginDisabled)); + Assert.That (client.AuthenticationMechanisms, Has.Count.EqualTo (5)); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH"), "Expected SASL XOAUTH auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH2"), "Expected SASL XOAUTH2 auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("OAUTHBEARER"), "Expected SASL OAUTHBEARER auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN"), "Expected SASL PLAIN auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN-CLIENTTOKEN"), "Expected SASL PLAIN-CLIENTTOKEN auth mechanism"); + + try { + client.Authenticate ("username", "password"); + Assert.Fail ("Did not expect Authenticate to work."); + } catch (AuthenticationException ax) { + Assert.That (ax.Message, Is.EqualTo ("AUTHENTICATE failed")); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + client.AuthenticationMechanisms.Clear (); + + try { + client.Authenticate ("username", "password"); + Assert.Fail ("Did not expect Authenticate to work."); + } catch (AuthenticationException ax) { + Assert.That (ax.Message, Is.EqualTo ("The LOGIN command is disabled.")); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + client.Disconnect (false); + } + } + + [Test] + public async Task TestLoginDisabledAsync () + { + var commands = CreateLoginDisabledCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.Capabilities, Is.EqualTo (GMailInitialCapabilities | ImapCapabilities.LoginDisabled)); + Assert.That (client.AuthenticationMechanisms, Has.Count.EqualTo (5)); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH"), "Expected SASL XOAUTH auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH2"), "Expected SASL XOAUTH2 auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("OAUTHBEARER"), "Expected SASL OAUTHBEARER auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN"), "Expected SASL PLAIN auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN-CLIENTTOKEN"), "Expected SASL PLAIN-CLIENTTOKEN auth mechanism"); + + try { + await client.AuthenticateAsync ("username", "password"); + Assert.Fail ("Did not expect Authenticate to work."); + } catch (AuthenticationException ax) { + Assert.That (ax.Message, Is.EqualTo ("AUTHENTICATE failed")); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + client.AuthenticationMechanisms.Clear (); + + try { + await client.AuthenticateAsync ("username", "password"); + Assert.Fail ("Did not expect Authenticate to work."); + } catch (AuthenticationException ax) { + Assert.That (ax.Message, Is.EqualTo ("The LOGIN command is disabled.")); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + await client.DisconnectAsync (false); + } + } + + static List CreateExchangeUserIsAuthenticatedButNotConnectedCommands () + { + return new List { + new ImapReplayCommand ("", "exchange.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "exchange.capability-preauth.txt"), + new ImapReplayCommand ("A00000001 LOGIN \"user@domain.com\\\\mailbox\" password\r\n", ImapReplayCommandResponse.OK), + new ImapReplayCommand ("A00000002 CAPABILITY\r\n", "exchange.capability-postauth.txt"), + new ImapReplayCommand ("A00000003 NAMESPACE\r\n", Encoding.ASCII.GetBytes ("A00000003 BAD User is authenticated but not connected.\r\n")) + }; + } + + [Test] + public void TestExchangeUserIsAuthenticatedButNotConnected () + { + var commands = CreateExchangeUserIsAuthenticatedButNotConnectedCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.Capabilities, Is.EqualTo (ImapCapabilities.IMAP4 | ImapCapabilities.IMAP4rev1 | ImapCapabilities.SaslIR | ImapCapabilities.UidPlus | ImapCapabilities.Id | + ImapCapabilities.Unselect | ImapCapabilities.Children | ImapCapabilities.Idle | ImapCapabilities.Namespace | ImapCapabilities.LiteralPlus | + ImapCapabilities.Status)); + Assert.That (client.AuthenticationMechanisms, Has.Count.EqualTo (2)); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH2"), "Expected SASL XOAUTH2 auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN"), "Expected SASL PLAIN auth mechanism"); + + client.AuthenticationMechanisms.Clear (); + + try { + client.Authenticate ("user@domain.com\\mailbox", "password"); + Assert.Fail ("Did not expect Authenticate to work."); + } catch (ImapCommandException cx) { + Assert.That (cx.Response, Is.EqualTo (ImapCommandResponse.Bad)); + Assert.That (cx.ResponseText, Is.EqualTo ("User is authenticated but not connected.")); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + client.Disconnect (false); + } + } + + [Test] + public async Task TestExchangeUserIsAuthenticatedButNotConnectedAsync () + { + var commands = CreateExchangeUserIsAuthenticatedButNotConnectedCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.Capabilities, Is.EqualTo (ImapCapabilities.IMAP4 | ImapCapabilities.IMAP4rev1 | ImapCapabilities.SaslIR | ImapCapabilities.UidPlus | ImapCapabilities.Id | + ImapCapabilities.Unselect | ImapCapabilities.Children | ImapCapabilities.Idle | ImapCapabilities.Namespace | ImapCapabilities.LiteralPlus | + ImapCapabilities.Status)); + Assert.That (client.AuthenticationMechanisms, Has.Count.EqualTo (2)); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH2"), "Expected SASL XOAUTH2 auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN"), "Expected SASL PLAIN auth mechanism"); + + client.AuthenticationMechanisms.Clear (); + + try { + await client.AuthenticateAsync ("user@domain.com\\mailbox", "password"); + Assert.Fail ("Did not expect Authenticate to work."); + } catch (ImapCommandException cx) { + Assert.That (cx.Response, Is.EqualTo (ImapCommandResponse.Bad)); + Assert.That (cx.ResponseText, Is.EqualTo ("User is authenticated but not connected.")); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + await client.DisconnectAsync (false); + } + } + + static List CreateAdvancedFeaturesCommands () + { + return new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability+login.txt"), + new ImapReplayCommand ("A00000001 AUTHENTICATE LOGIN\r\n", ImapReplayCommandResponse.Plus), + new ImapReplayCommand ("dXNlcm5hbWU=\r\n", ImapReplayCommandResponse.Plus), + new ImapReplayCommand ("A00000001", "cGFzc3dvcmQ=\r\n", "gmail.authenticate.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-inbox.txt"), + new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt"), + new ImapReplayCommand ("A00000005 ENABLE UTF8=ACCEPT\r\n", "gmail.utf8accept.txt"), + new ImapReplayCommand ("A00000006 GETQUOTAROOT INBOX\r\n", "common.getquota.txt"), + new ImapReplayCommand ("A00000007 SETQUOTA \"\" (MESSAGE 1000000 STORAGE 5242880)\r\n", "common.setquota.txt") + }; + } + + [Test] + public void TestAdvancedFeatures () + { + var commands = CreateAdvancedFeaturesCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + Assert.That (client.IsSecure, Is.False, "IsSecure should be false."); + + Assert.That (client.Capabilities, Is.EqualTo (GMailInitialCapabilities)); + Assert.That (client.AuthenticationMechanisms, Has.Count.EqualTo (6)); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH"), "Expected SASL XOAUTH auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH2"), "Expected SASL XOAUTH2 auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("OAUTHBEARER"), "Expected SASL OAUTHBEARER auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN"), "Expected SASL PLAIN auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN-CLIENTTOKEN"), "Expected SASL PLAIN-CLIENTTOKEN auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("LOGIN"), "Expected SASL LOGIN auth mechanism"); + + Assert.That (client.Timeout, Is.EqualTo (120000), "Timeout"); + client.Timeout *= 2; + + // Note: Do not try XOAUTH2 or PLAIN + client.AuthenticationMechanisms.Remove ("XOAUTH2"); + client.AuthenticationMechanisms.Remove ("PLAIN"); + + int authenticated = 0; + client.Authenticated += (sender, e) => { + authenticated++; + }; + + try { + client.Authenticate (new NetworkCredential ("username", "password")); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (authenticated, Is.EqualTo (1), "Authenticated event was not emitted the expected number of times"); + Assert.That (client.Capabilities, Is.EqualTo (GMailAuthenticatedCapabilities)); + Assert.That (client.SupportsQuotas, Is.True, "SupportsQuotas"); + + client.EnableUTF8 (); + + var personal = client.GetFolder (client.PersonalNamespaces[0]); + var inbox = client.Inbox; + + Assert.That (inbox, Is.Not.Null, "Expected non-null Inbox folder."); + Assert.That (inbox.Attributes, Is.EqualTo (FolderAttributes.Inbox | FolderAttributes.HasNoChildren | FolderAttributes.Subscribed), "Expected Inbox attributes to be \\HasNoChildren."); + + var quota = inbox.GetQuota (); + Assert.That (quota, Is.Not.Null, "Expected a non-null GETQUOTAROOT response."); + Assert.That (quota.QuotaRoot.FullName, Is.EqualTo (personal.FullName)); + Assert.That (quota.QuotaRoot, Is.EqualTo (personal)); + Assert.That (quota.CurrentStorageSize.Value, Is.EqualTo (3783)); + Assert.That (quota.StorageLimit.Value, Is.EqualTo (15728640)); + Assert.That (quota.CurrentMessageCount.HasValue, Is.False); + Assert.That (quota.MessageLimit.HasValue, Is.False); + + quota = personal.SetQuota (1000000, 5242880); + Assert.That (quota, Is.Not.Null, "Expected non-null SETQUOTA response."); + Assert.That (quota.CurrentMessageCount.Value, Is.EqualTo (1107)); + Assert.That (quota.CurrentStorageSize.Value, Is.EqualTo (3783)); + Assert.That (quota.MessageLimit.Value, Is.EqualTo (1000000)); + Assert.That (quota.StorageLimit.Value, Is.EqualTo (5242880)); + + client.Disconnect (false); + } + } + + [Test] + public async Task TestAdvancedFeaturesAsync () + { + var commands = CreateAdvancedFeaturesCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + Assert.That (client.IsSecure, Is.False, "IsSecure should be false."); + + Assert.That (client.Capabilities, Is.EqualTo (GMailInitialCapabilities)); + Assert.That (client.AuthenticationMechanisms, Has.Count.EqualTo (6)); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH"), "Expected SASL XOAUTH auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH2"), "Expected SASL XOAUTH2 auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("OAUTHBEARER"), "Expected SASL OAUTHBEARER auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN"), "Expected SASL PLAIN auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN-CLIENTTOKEN"), "Expected SASL PLAIN-CLIENTTOKEN auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("LOGIN"), "Expected SASL LOGIN auth mechanism"); + + Assert.That (client.Timeout, Is.EqualTo (120000), "Timeout"); + client.Timeout *= 2; + + // Note: Do not try XOAUTH2 or PLAIN + client.AuthenticationMechanisms.Remove ("XOAUTH2"); + client.AuthenticationMechanisms.Remove ("PLAIN"); + + int authenticated = 0; + client.Authenticated += (sender, e) => { + authenticated++; + }; + + try { + await client.AuthenticateAsync (new NetworkCredential ("username", "password")); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (authenticated, Is.EqualTo (1), "Authenticated event was not emitted the expected number of times"); + Assert.That (client.Capabilities, Is.EqualTo (GMailAuthenticatedCapabilities)); + Assert.That (client.SupportsQuotas, Is.True, "SupportsQuotas"); + + await client.EnableUTF8Async (); + + var personal = client.GetFolder (client.PersonalNamespaces[0]); + var inbox = client.Inbox; + + Assert.That (inbox, Is.Not.Null, "Expected non-null Inbox folder."); + Assert.That (inbox.Attributes, Is.EqualTo (FolderAttributes.Inbox | FolderAttributes.HasNoChildren | FolderAttributes.Subscribed), "Expected Inbox attributes to be \\HasNoChildren."); + + var quota = await inbox.GetQuotaAsync (); + Assert.That (quota, Is.Not.Null, "Expected a non-null GETQUOTAROOT response."); + Assert.That (quota.QuotaRoot.FullName, Is.EqualTo (personal.FullName)); + Assert.That (quota.QuotaRoot, Is.EqualTo (personal)); + Assert.That (quota.CurrentStorageSize.Value, Is.EqualTo (3783)); + Assert.That (quota.StorageLimit.Value, Is.EqualTo (15728640)); + Assert.That (quota.CurrentMessageCount.HasValue, Is.False); + Assert.That (quota.MessageLimit.HasValue, Is.False); + + quota = await personal.SetQuotaAsync (1000000, 5242880); + Assert.That (quota, Is.Not.Null, "Expected non-null SETQUOTA response."); + Assert.That (quota.CurrentMessageCount.Value, Is.EqualTo (1107)); + Assert.That (quota.CurrentStorageSize.Value, Is.EqualTo (3783)); + Assert.That (quota.MessageLimit.Value, Is.EqualTo (1000000)); + Assert.That (quota.StorageLimit.Value, Is.EqualTo (5242880)); + + await client.DisconnectAsync (false); + } + } + + static List CreateSendingStringsAsLiteralsCommands () + { + return new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability+login.txt"), + new ImapReplayCommand ("A00000001 AUTHENTICATE LOGIN\r\n", ImapReplayCommandResponse.Plus), + new ImapReplayCommand ("dXNlcm5hbWU=\r\n", ImapReplayCommandResponse.Plus), + new ImapReplayCommand ("A00000001", "cGFzc3dvcmQ=\r\n", "gmail.authenticate.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-inbox.txt"), + new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt"), + new ImapReplayCommand ("A00000005 ENABLE UTF8=ACCEPT\r\n", "gmail.utf8accept.txt"), + new ImapReplayCommand ("A00000006 ID (\"name\" \"MailKit\" \"version\" \"1.0\" \"vendor\" \"Xamarin Inc.\" \"address\" {35}\r\n", ImapReplayCommandResponse.Plus), + new ImapReplayCommand ("A00000006", "1 Memorial Dr.\r\nCambridge, MA 02142)\r\n", "common.id.txt"), + new ImapReplayCommand ("A00000007 LOGOUT\r\n", "gmail.logout.txt") + }; + } + + [Test] + public void TestSendingStringsAsLiterals () + { + var commands = CreateSendingStringsAsLiteralsCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: Do not try XOAUTH2 or PLAIN + client.AuthenticationMechanisms.Remove ("XOAUTH2"); + client.AuthenticationMechanisms.Remove ("PLAIN"); + + try { + client.Authenticate (new NetworkCredential ("username", "password")); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + client.EnableUTF8 (); + + var implementation = new ImapImplementation { + Name = "MailKit", Version = "1.0", Vendor = "Xamarin Inc.", Address = "1 Memorial Dr.\r\nCambridge, MA 02142" + }; + + // Disable LITERAL+ and LITERAL- extensions + client.Capabilities &= ~ImapCapabilities.LiteralPlus; + client.Capabilities &= ~ImapCapabilities.LiteralMinus; + + implementation = client.Identify (implementation); + + client.Disconnect (true); + } + } + + [Test] + public async Task TestSendingStringsAsLiteralsAsync () + { + var commands = CreateSendingStringsAsLiteralsCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: Do not try XOAUTH2 or PLAIN + client.AuthenticationMechanisms.Remove ("XOAUTH2"); + client.AuthenticationMechanisms.Remove ("PLAIN"); + + try { + await client.AuthenticateAsync (new NetworkCredential ("username", "password")); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + await client.EnableUTF8Async (); + + var implementation = new ImapImplementation { + Name = "MailKit", Version = "1.0", Vendor = "Xamarin Inc.", Address = "1 Memorial Dr.\r\nCambridge, MA 02142" + }; + + // Disable LITERAL+ and LITERAL- extensions + client.Capabilities &= ~ImapCapabilities.LiteralPlus; + client.Capabilities &= ~ImapCapabilities.LiteralMinus; + + implementation = await client.IdentifyAsync (implementation); + + await client.DisconnectAsync (true); + } + } + + static List CreateSaslAuthenticationCommands () + { + return new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability+login.txt"), + new ImapReplayCommand ("A00000001 AUTHENTICATE LOGIN\r\n", ImapReplayCommandResponse.Plus), + new ImapReplayCommand ("dXNlcm5hbWU=\r\n", ImapReplayCommandResponse.Plus), + new ImapReplayCommand ("A00000001", "cGFzc3dvcmQ=\r\n", "gmail.authenticate.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-inbox.txt"), + new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt") + }; + } + + [Test] + public void TestSaslAuthentication () + { + var commands = CreateSaslAuthenticationCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + Assert.That (client.IsSecure, Is.False, "IsSecure should be false."); + + Assert.That (client.Capabilities, Is.EqualTo (GMailInitialCapabilities)); + Assert.That (client.AuthenticationMechanisms, Has.Count.EqualTo (6)); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH"), "Expected SASL XOAUTH auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH2"), "Expected SASL XOAUTH2 auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("OAUTHBEARER"), "Expected SASL OAUTHBEARER auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN"), "Expected SASL PLAIN auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN-CLIENTTOKEN"), "Expected SASL PLAIN-CLIENTTOKEN auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("LOGIN"), "Expected SASL LOGIN auth mechanism"); + + Assert.That (client.Timeout, Is.EqualTo (120000), "Timeout"); + client.Timeout *= 2; + + int authenticated = 0; + client.Authenticated += (sender, e) => { + authenticated++; + }; + + try { + var credentials = new NetworkCredential ("username", "password"); + var sasl = new SaslMechanismLogin (credentials); + + client.Authenticate (sasl); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (authenticated, Is.EqualTo (1), "Authenticated event was not emitted the expected number of times"); + Assert.That (client.Capabilities, Is.EqualTo (GMailAuthenticatedCapabilities)); + Assert.That (client.SupportsQuotas, Is.True, "SupportsQuotas"); + + client.Disconnect (false); + } + } + + [Test] + public async Task TestSaslAuthenticationAsync () + { + var commands = CreateSaslAuthenticationCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + Assert.That (client.IsSecure, Is.False, "IsSecure should be false."); + + Assert.That (client.Capabilities, Is.EqualTo (GMailInitialCapabilities)); + Assert.That (client.AuthenticationMechanisms, Has.Count.EqualTo (6)); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH"), "Expected SASL XOAUTH auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH2"), "Expected SASL XOAUTH2 auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("OAUTHBEARER"), "Expected SASL OAUTHBEARER auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN"), "Expected SASL PLAIN auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN-CLIENTTOKEN"), "Expected SASL PLAIN-CLIENTTOKEN auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("LOGIN"), "Expected SASL LOGIN auth mechanism"); + + Assert.That (client.Timeout, Is.EqualTo (120000), "Timeout"); + client.Timeout *= 2; + + int authenticated = 0; + client.Authenticated += (sender, e) => { + authenticated++; + }; + + try { + var credentials = new NetworkCredential ("username", "password"); + var sasl = new SaslMechanismLogin (credentials); + + await client.AuthenticateAsync (sasl); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (authenticated, Is.EqualTo (1), "Authenticated event was not emitted the expected number of times"); + Assert.That (client.Capabilities, Is.EqualTo (GMailAuthenticatedCapabilities)); + Assert.That (client.SupportsQuotas, Is.True, "SupportsQuotas"); + + await client.DisconnectAsync (false); + } + } + + static List CreateSaslIRAuthenticationCommands () + { + return new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt"), + new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "gmail.authenticate.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-inbox.txt"), + new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt") + }; + } + + [Test] + public void TestSaslIRAuthentication () + { + var commands = CreateSaslIRAuthenticationCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + Assert.That (client.IsSecure, Is.False, "IsSecure should be false."); + + Assert.That (client.Capabilities, Is.EqualTo (GMailInitialCapabilities)); + Assert.That (client.AuthenticationMechanisms, Has.Count.EqualTo (5)); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH"), "Expected SASL XOAUTH auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH2"), "Expected SASL XOAUTH2 auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("OAUTHBEARER"), "Expected SASL OAUTHBEARER auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN"), "Expected SASL PLAIN auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN-CLIENTTOKEN"), "Expected SASL PLAIN-CLIENTTOKEN auth mechanism"); + + Assert.That (client.Timeout, Is.EqualTo (120000), "Timeout"); + client.Timeout *= 2; + + int authenticated = 0; + client.Authenticated += (sender, e) => { + authenticated++; + }; + + try { + var credentials = new NetworkCredential ("username", "password"); + var sasl = new SaslMechanismPlain (credentials); + + client.Authenticate (sasl); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (authenticated, Is.EqualTo (1), "Authenticated event was not emitted the expected number of times"); + Assert.That (client.Capabilities, Is.EqualTo (GMailAuthenticatedCapabilities)); + Assert.That (client.SupportsQuotas, Is.True, "SupportsQuotas"); + + client.Disconnect (false); + } + } + + [Test] + public async Task TestSaslIRAuthenticationAsync () + { + var commands = CreateSaslIRAuthenticationCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + Assert.That (client.IsSecure, Is.False, "IsSecure should be false."); + + Assert.That (client.Capabilities, Is.EqualTo (GMailInitialCapabilities)); + Assert.That (client.AuthenticationMechanisms, Has.Count.EqualTo (5)); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH"), "Expected SASL XOAUTH auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH2"), "Expected SASL XOAUTH2 auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("OAUTHBEARER"), "Expected SASL OAUTHBEARER auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN"), "Expected SASL PLAIN auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN-CLIENTTOKEN"), "Expected SASL PLAIN-CLIENTTOKEN auth mechanism"); + + Assert.That (client.Timeout, Is.EqualTo (120000), "Timeout"); + client.Timeout *= 2; + + int authenticated = 0; + client.Authenticated += (sender, e) => { + authenticated++; + }; + + try { + var credentials = new NetworkCredential ("username", "password"); + var sasl = new SaslMechanismPlain (credentials); + + await client.AuthenticateAsync (sasl); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (authenticated, Is.EqualTo (1), "Authenticated event was not emitted the expected number of times"); + Assert.That (client.Capabilities, Is.EqualTo (GMailAuthenticatedCapabilities)); + Assert.That (client.SupportsQuotas, Is.True, "SupportsQuotas"); + + await client.DisconnectAsync (false); + } + } + + static void AssertRedacted (MemoryStream stream, string commandPrefix, string nextCommandPrefix) + { + stream.Position = 0; + + using (var reader = new StreamReader (stream, Encoding.ASCII, false, 1024, true)) { + string secrets; + string line; + + while ((line = reader.ReadLine ()) != null) { + if (line.StartsWith (commandPrefix, StringComparison.Ordinal)) + break; + } + + Assert.That (line, Is.Not.Null, $"Authentication command not found: {commandPrefix}"); + + if (line.Length > commandPrefix.Length) { + secrets = line.Substring (commandPrefix.Length); + + var tokens = secrets.Split (' '); + var expectedTokens = new string[tokens.Length]; + for (int i = 0; i < tokens.Length; i++) { + if (tokens[i][0] == '"') + expectedTokens[i] = "\"********\""; + else + expectedTokens[i] = "********"; + } + + var expected = string.Join (" ", expectedTokens); + + Assert.That (secrets, Is.EqualTo (expected), commandPrefix); + } + + while ((line = reader.ReadLine ()) != null) { + if (line.StartsWith (nextCommandPrefix, StringComparison.Ordinal)) + return; + + if (!line.StartsWith ("C: ", StringComparison.Ordinal)) + continue; + + secrets = line.Substring (3); + + Assert.That (secrets, Is.EqualTo ("********"), "SASL challenge"); + } + + Assert.Fail ("Did not find response."); + } + } + + static List CreateRedactLoginCommands () + { + return new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt"), + new ImapReplayCommand ("A00000001 LOGIN username \"pass%word\"\r\n", "gmail.authenticate.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-inbox.txt"), + new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt") + }; + } + + [Test] + public void TestRedactLogin () + { + var commands = CreateRedactLoginCommands (); + + using (var stream = new MemoryStream ()) { + using (var client = new ImapClient (new ProtocolLogger (stream, true) { RedactSecrets = true }) { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + Assert.That (client.IsSecure, Is.False, "IsSecure should be false."); + + Assert.That (client.Capabilities, Is.EqualTo (GMailInitialCapabilities)); + Assert.That (client.AuthenticationMechanisms, Has.Count.EqualTo (5)); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH"), "Expected SASL XOAUTH auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH2"), "Expected SASL XOAUTH2 auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("OAUTHBEARER"), "Expected SASL OAUTHBEARER auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN"), "Expected SASL PLAIN auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN-CLIENTTOKEN"), "Expected SASL PLAIN-CLIENTTOKEN auth mechanism"); + + Assert.That (client.Timeout, Is.EqualTo (120000), "Timeout"); + client.Timeout *= 2; + + // Note: Do not try to use any SASL mechanisms + client.AuthenticationMechanisms.Clear (); + + int authenticated = 0; + client.Authenticated += (sender, e) => { + authenticated++; + }; + + try { + client.Authenticate ("username", "pass%word"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (authenticated, Is.EqualTo (1), "Authenticated event was not emitted the expected number of times"); + Assert.That (client.Capabilities, Is.EqualTo (GMailAuthenticatedCapabilities)); + Assert.That (client.SupportsQuotas, Is.True, "SupportsQuotas"); + + client.Disconnect (false); + } + + AssertRedacted (stream, "C: A00000001 LOGIN ", "C: A00000002 NAMESPACE"); + } + } + + [Test] + public async Task TestRedactLoginAsync () + { + var commands = CreateRedactLoginCommands (); + + using (var stream = new MemoryStream ()) { + using (var client = new ImapClient (new ProtocolLogger (stream, true) { RedactSecrets = true }) { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + Assert.That (client.IsSecure, Is.False, "IsSecure should be false."); + + Assert.That (client.Capabilities, Is.EqualTo (GMailInitialCapabilities)); + Assert.That (client.AuthenticationMechanisms, Has.Count.EqualTo (5)); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH"), "Expected SASL XOAUTH auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH2"), "Expected SASL XOAUTH2 auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("OAUTHBEARER"), "Expected SASL OAUTHBEARER auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN"), "Expected SASL PLAIN auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN-CLIENTTOKEN"), "Expected SASL PLAIN-CLIENTTOKEN auth mechanism"); + + Assert.That (client.Timeout, Is.EqualTo (120000), "Timeout"); + client.Timeout *= 2; + + // Note: Do not try to use any SASL mechanisms + client.AuthenticationMechanisms.Clear (); + + int authenticated = 0; + client.Authenticated += (sender, e) => { + authenticated++; + }; + + try { + await client.AuthenticateAsync ("username", "pass%word"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (authenticated, Is.EqualTo (1), "Authenticated event was not emitted the expected number of times"); + Assert.That (client.Capabilities, Is.EqualTo (GMailAuthenticatedCapabilities)); + Assert.That (client.SupportsQuotas, Is.True, "SupportsQuotas"); + + await client.DisconnectAsync (false); + } + + AssertRedacted (stream, "C: A00000001 LOGIN ", "C: A00000002 NAMESPACE"); + } + } + + static List CreateRedactAuthenticationCommands () + { + return new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt"), + new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "gmail.authenticate.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-inbox.txt"), + new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt") + }; + } + + [Test] + public void TestRedactAuthentication () + { + var commands = CreateRedactAuthenticationCommands (); + + using (var stream = new MemoryStream ()) { + using (var client = new ImapClient (new ProtocolLogger (stream, true) { RedactSecrets = true }) { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + + Assert.That (client.Capabilities, Is.EqualTo (GMailInitialCapabilities)); + Assert.That (client.AuthenticationMechanisms, Has.Count.EqualTo (5)); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH"), "Expected SASL XOAUTH auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH2"), "Expected SASL XOAUTH2 auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("OAUTHBEARER"), "Expected SASL OAUTHBEARER auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN"), "Expected SASL PLAIN auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN-CLIENTTOKEN"), "Expected SASL PLAIN-CLIENTTOKEN auth mechanism"); + + // Note: Do not try XOAUTH2 + client.AuthenticationMechanisms.Remove ("XOAUTH2"); + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities, Is.EqualTo (GMailAuthenticatedCapabilities)); + + client.Disconnect (false); + } + + AssertRedacted (stream, "C: A00000001 AUTHENTICATE PLAIN ", "C: A00000002 NAMESPACE"); + } + } + + [Test] + public async Task TestRedactAuthenticationAsync () + { + var commands = CreateRedactAuthenticationCommands (); + + using (var stream = new MemoryStream ()) { + using (var client = new ImapClient (new ProtocolLogger (stream, true) { RedactSecrets = true }) { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + + Assert.That (client.Capabilities, Is.EqualTo (GMailInitialCapabilities)); + Assert.That (client.AuthenticationMechanisms, Has.Count.EqualTo (5)); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH"), "Expected SASL XOAUTH auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH2"), "Expected SASL XOAUTH2 auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("OAUTHBEARER"), "Expected SASL OAUTHBEARER auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN"), "Expected SASL PLAIN auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN-CLIENTTOKEN"), "Expected SASL PLAIN-CLIENTTOKEN auth mechanism"); + + // Note: Do not try XOAUTH2 + client.AuthenticationMechanisms.Remove ("XOAUTH2"); + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities, Is.EqualTo (GMailAuthenticatedCapabilities)); + + await client.DisconnectAsync (false); + } + + AssertRedacted (stream, "C: A00000001 AUTHENTICATE PLAIN ", "C: A00000002 NAMESPACE"); + } + } + + static List CreateRedactSaslAuthenticationCommands () + { + return new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability+login.txt"), + new ImapReplayCommand ("A00000001 AUTHENTICATE LOGIN\r\n", ImapReplayCommandResponse.Plus), + new ImapReplayCommand ("dXNlcm5hbWU=\r\n", ImapReplayCommandResponse.Plus), + new ImapReplayCommand ("A00000001", "cGFzc3dvcmQ=\r\n", "gmail.authenticate.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-inbox.txt"), + new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt") + }; + } + + [Test] + public void TestRedactSaslAuthentication () + { + var commands = CreateRedactSaslAuthenticationCommands (); + + using (var stream = new MemoryStream ()) { + using (var client = new ImapClient (new ProtocolLogger (stream, true) { RedactSecrets = true }) { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + Assert.That (client.IsSecure, Is.False, "IsSecure should be false."); + + Assert.That (client.Capabilities, Is.EqualTo (GMailInitialCapabilities)); + Assert.That (client.AuthenticationMechanisms, Has.Count.EqualTo (6)); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH"), "Expected SASL XOAUTH auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH2"), "Expected SASL XOAUTH2 auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("OAUTHBEARER"), "Expected SASL OAUTHBEARER auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN"), "Expected SASL PLAIN auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN-CLIENTTOKEN"), "Expected SASL PLAIN-CLIENTTOKEN auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("LOGIN"), "Expected SASL LOGIN auth mechanism"); + + Assert.That (client.Timeout, Is.EqualTo (120000), "Timeout"); + client.Timeout *= 2; + + int authenticated = 0; + client.Authenticated += (sender, e) => { + authenticated++; + }; + + try { + var credentials = new NetworkCredential ("username", "password"); + var sasl = new SaslMechanismLogin (credentials); + + client.Authenticate (sasl); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (authenticated, Is.EqualTo (1), "Authenticated event was not emitted the expected number of times"); + Assert.That (client.Capabilities, Is.EqualTo (GMailAuthenticatedCapabilities)); + Assert.That (client.SupportsQuotas, Is.True, "SupportsQuotas"); + + client.Disconnect (false); + } + + AssertRedacted (stream, "C: A00000001 AUTHENTICATE LOGIN", "C: A00000002 NAMESPACE"); + } + } + + [Test] + public async Task TestRedactSaslAuthenticationAsync () + { + var commands = CreateRedactSaslAuthenticationCommands (); + + using (var stream = new MemoryStream ()) { + using (var client = new ImapClient (new ProtocolLogger (stream, true) { RedactSecrets = true }) { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + Assert.That (client.IsSecure, Is.False, "IsSecure should be false."); + + Assert.That (client.Capabilities, Is.EqualTo (GMailInitialCapabilities)); + Assert.That (client.AuthenticationMechanisms, Has.Count.EqualTo (6)); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH"), "Expected SASL XOAUTH auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH2"), "Expected SASL XOAUTH2 auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("OAUTHBEARER"), "Expected SASL OAUTHBEARER auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN"), "Expected SASL PLAIN auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN-CLIENTTOKEN"), "Expected SASL PLAIN-CLIENTTOKEN auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("LOGIN"), "Expected SASL LOGIN auth mechanism"); + + Assert.That (client.Timeout, Is.EqualTo (120000), "Timeout"); + client.Timeout *= 2; + + int authenticated = 0; + client.Authenticated += (sender, e) => { + authenticated++; + }; + + try { + var credentials = new NetworkCredential ("username", "password"); + var sasl = new SaslMechanismLogin (credentials); + + await client.AuthenticateAsync (sasl); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (authenticated, Is.EqualTo (1), "Authenticated event was not emitted the expected number of times"); + Assert.That (client.Capabilities, Is.EqualTo (GMailAuthenticatedCapabilities)); + Assert.That (client.SupportsQuotas, Is.True, "SupportsQuotas"); + + await client.DisconnectAsync (false); + } + + AssertRedacted (stream, "C: A00000001 AUTHENTICATE LOGIN", "C: A00000002 NAMESPACE"); + } + } + + static List CreateEnableUTF8Commands () + { + return new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt"), + new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "gmail.authenticate.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-inbox.txt"), + new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt"), + new ImapReplayCommand ("A00000005 ENABLE UTF8=ACCEPT\r\n", "gmail.utf8accept.txt") + }; + } + + [Test] + public void TestEnableUTF8 () + { + var commands = CreateEnableUTF8Commands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + Assert.That (client.IsSecure, Is.False, "IsSecure should be false."); + + Assert.That (client.Capabilities, Is.EqualTo (GMailInitialCapabilities)); + Assert.That (client.AuthenticationMechanisms, Has.Count.EqualTo (5)); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH"), "Expected SASL XOAUTH auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH2"), "Expected SASL XOAUTH2 auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("OAUTHBEARER"), "Expected SASL OAUTHBEARER auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN"), "Expected SASL PLAIN auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN-CLIENTTOKEN"), "Expected SASL PLAIN-CLIENTTOKEN auth mechanism"); + + try { + client.Authenticate (new NetworkCredential ("username", "password")); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities, Is.EqualTo (GMailAuthenticatedCapabilities)); + Assert.That (client.SupportsQuotas, Is.True, "SupportsQuotas"); + + client.EnableUTF8 (); + + // ENABLE UTF8 a second time should no-op. + client.EnableUTF8 (); + + client.Disconnect (false); + } + } + + [Test] + public async Task TestEnableUTF8Async () + { + var commands = CreateEnableUTF8Commands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + Assert.That (client.IsSecure, Is.False, "IsSecure should be false."); + + Assert.That (client.Capabilities, Is.EqualTo (GMailInitialCapabilities)); + Assert.That (client.AuthenticationMechanisms, Has.Count.EqualTo (5)); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH"), "Expected SASL XOAUTH auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH2"), "Expected SASL XOAUTH2 auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("OAUTHBEARER"), "Expected SASL OAUTHBEARER auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN"), "Expected SASL PLAIN auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN-CLIENTTOKEN"), "Expected SASL PLAIN-CLIENTTOKEN auth mechanism"); + + try { + await client.AuthenticateAsync (new NetworkCredential ("username", "password")); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities, Is.EqualTo (GMailAuthenticatedCapabilities)); + Assert.That (client.SupportsQuotas, Is.True, "SupportsQuotas"); + + await client.EnableUTF8Async (); + + // ENABLE UTF8 a second time should no-op. + await client.EnableUTF8Async (); + + await client.DisconnectAsync (false); + } + } + + static List CreateEnableQuickResyncCommands () + { + return new List { + new ImapReplayCommand ("", "dovecot.greeting.txt"), + new ImapReplayCommand ("A00000000 LOGIN username password\r\n", "dovecot.authenticate.txt"), + new ImapReplayCommand ("A00000001 NAMESPACE\r\n", "dovecot.namespace.txt"), + new ImapReplayCommand ("A00000002 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-inbox.txt"), + new ImapReplayCommand ("A00000003 LIST (SPECIAL-USE) \"\" \"*\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-special-use.txt"), + new ImapReplayCommand ("A00000004 ENABLE QRESYNC CONDSTORE\r\n", "dovecot.enable-qresync.txt"), + }; + } + + [Test] + public void TestEnableQuickResync () + { + var commands = CreateEnableQuickResyncCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.Capabilities, Is.EqualTo (DovecotInitialCapabilities)); + Assert.That (client.AuthenticationMechanisms, Has.Count.EqualTo (4)); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN"), "Expected SASL PLAIN auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("DIGEST-MD5"), "Expected SASL DIGEST-MD5 auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("CRAM-MD5"), "Expected SASL CRAM-MD5 auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("NTLM"), "Expected SASL NTLM auth mechanism"); + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities, Is.EqualTo (DovecotAuthenticatedCapabilities)); + Assert.That (client.InternationalizationLevel, Is.EqualTo (1), "Expected I18NLEVEL=1"); + Assert.That (client.ThreadingAlgorithms, Does.Contain (ThreadingAlgorithm.OrderedSubject), "Expected THREAD=ORDEREDSUBJECT"); + Assert.That (client.ThreadingAlgorithms, Does.Contain (ThreadingAlgorithm.References), "Expected THREAD=REFERENCES"); + + client.EnableQuickResync (); + + Assert.That (client.Inbox.Supports (FolderFeature.QuickResync), Is.True, "Expected the INBOX to support QRESYNC"); + + // ENABLE QRESYNC a second time should no-op. + client.EnableQuickResync (); + + client.Disconnect (false); + } + } + + [Test] + public async Task TestEnableQuickResyncAsync () + { + var commands = CreateEnableQuickResyncCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.Capabilities, Is.EqualTo (DovecotInitialCapabilities)); + Assert.That (client.AuthenticationMechanisms, Has.Count.EqualTo (4)); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN"), "Expected SASL PLAIN auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("DIGEST-MD5"), "Expected SASL DIGEST-MD5 auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("CRAM-MD5"), "Expected SASL CRAM-MD5 auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("NTLM"), "Expected SASL NTLM auth mechanism"); + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities, Is.EqualTo (DovecotAuthenticatedCapabilities)); + Assert.That (client.InternationalizationLevel, Is.EqualTo (1), "Expected I18NLEVEL=1"); + Assert.That (client.ThreadingAlgorithms, Does.Contain (ThreadingAlgorithm.OrderedSubject), "Expected THREAD=ORDEREDSUBJECT"); + Assert.That (client.ThreadingAlgorithms, Does.Contain (ThreadingAlgorithm.References), "Expected THREAD=REFERENCES"); + + await client.EnableQuickResyncAsync (); + + Assert.That (client.Inbox.Supports (FolderFeature.QuickResync), Is.True, "Expected the INBOX to support QRESYNC"); + + // ENABLE QRESYNC a second time should no-op. + await client.EnableQuickResyncAsync (); + + await client.DisconnectAsync (false); + } + } + + static List CreateEnableQuickResynciCloudCommands () + { + return new List { + new ImapReplayCommand ("", "icloud.greeting.txt"), + new ImapReplayCommand ("A00000000 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "icloud.authenticate-plain.txt"), + new ImapReplayCommand ("A00000001 CAPABILITY\r\n", "icloud.capability.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "icloud.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\"\r\n", "icloud.list-inbox.txt"), + new ImapReplayCommand ("A00000004 ENABLE QRESYNC CONDSTORE\r\n", "icloud.enable-qresync.txt"), + }; + } + + [Test] + public void TestEnableQuickResynciCloud () + { + var commands = CreateEnableQuickResynciCloudCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.Capabilities, Is.EqualTo (ICloudInitialCapabilities)); + Assert.That (client.AuthenticationMechanisms, Has.Count.EqualTo (4)); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("ATOKEN"), "Expected SASL ATOKEN auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN"), "Expected SASL PLAIN auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("ATOKEN2"), "Expected SASL ATOKEN2 auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH2"), "Expected SASL XOAUTH2 auth mechanism"); + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities, Is.EqualTo (ICloudAuthenticatedCapabilities)); + Assert.That (client.ThreadingAlgorithms, Does.Contain (ThreadingAlgorithm.OrderedSubject), "Expected THREAD=ORDEREDSUBJECT"); + Assert.That (client.ThreadingAlgorithms, Does.Contain (ThreadingAlgorithm.References), "Expected THREAD=REFERENCES"); + + client.EnableQuickResync (); + + Assert.That (client.Inbox.Supports (FolderFeature.QuickResync), Is.True, "Expected the INBOX to support QRESYNC"); + + // ENABLE QRESYNC a second time should no-op. + client.EnableQuickResync (); + + client.Disconnect (false); + } + } + + [Test] + public async Task TestEnableQuickResynciCloudAsync () + { + var commands = CreateEnableQuickResynciCloudCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.Capabilities, Is.EqualTo (ICloudInitialCapabilities)); + Assert.That (client.AuthenticationMechanisms, Has.Count.EqualTo (4)); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("ATOKEN"), "Expected SASL ATOKEN auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN"), "Expected SASL PLAIN auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("ATOKEN2"), "Expected SASL ATOKEN2 auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH2"), "Expected SASL XOAUTH2 auth mechanism"); + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities, Is.EqualTo (ICloudAuthenticatedCapabilities)); + Assert.That (client.ThreadingAlgorithms, Does.Contain (ThreadingAlgorithm.OrderedSubject), "Expected THREAD=ORDEREDSUBJECT"); + Assert.That (client.ThreadingAlgorithms, Does.Contain (ThreadingAlgorithm.References), "Expected THREAD=REFERENCES"); + + await client.EnableQuickResyncAsync (); + + Assert.That (client.Inbox.Supports (FolderFeature.QuickResync), Is.True, "Expected the INBOX to support QRESYNC"); + + // ENABLE QRESYNC a second time should no-op. + await client.EnableQuickResyncAsync (); + + await client.DisconnectAsync (false); + } + } + + static void AssertFolder (IMailFolder folder, string fullName, string id, FolderAttributes attributes, bool subscribed, ulong highestmodseq, int count, int recent, uint uidnext, uint validity, int unread, ulong size) + { + if (subscribed) + attributes |= FolderAttributes.Subscribed; + + Assert.That (folder.FullName, Is.EqualTo (fullName), "FullName"); + Assert.That (folder.Attributes, Is.EqualTo (attributes), "Attributes"); + Assert.That (folder.IsSubscribed, Is.EqualTo (subscribed), "IsSubscribed"); + Assert.That (folder.HighestModSeq, Is.EqualTo (highestmodseq), "HighestModSeq"); + Assert.That (folder, Has.Count.EqualTo (count), "Count"); + Assert.That (folder.Recent, Is.EqualTo (recent), "Recent"); + Assert.That (folder.Unread, Is.EqualTo (unread), "Unread"); + Assert.That (folder.UidNext.HasValue ? folder.UidNext.Value.Id : (uint) 0, Is.EqualTo (uidnext), "UidNext"); + Assert.That (folder.UidValidity, Is.EqualTo (validity), "UidValidity"); + Assert.That (folder.Size ?? (ulong) 0, Is.EqualTo (size), "Size"); + Assert.That (folder.Id, Is.EqualTo (id), "MailboxId"); + } + + static List CreateGetFoldersCommands () + { + return new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt"), + new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "gmail.authenticate+statussize+objectid.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-inbox.txt"), + new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt"), + new ImapReplayCommand ("A00000005 LIST (SUBSCRIBED) \"\" \"*\" RETURN (CHILDREN STATUS (MESSAGES RECENT UIDNEXT UIDVALIDITY UNSEEN HIGHESTMODSEQ SIZE MAILBOXID))\r\n", "gmail.list-all.txt"), + new ImapReplayCommand ("A00000006 LIST \"\" \"*\" RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-all-no-status.txt"), + new ImapReplayCommand ("A00000007 STATUS INBOX (MESSAGES RECENT UIDNEXT UIDVALIDITY UNSEEN HIGHESTMODSEQ SIZE MAILBOXID)\r\n", "gmail.status-inbox.txt"), + new ImapReplayCommand ("A00000008 STATUS +Folder (MESSAGES RECENT UIDNEXT UIDVALIDITY UNSEEN HIGHESTMODSEQ SIZE MAILBOXID)\r\n", "gmail.status-+folder.txt"), + new ImapReplayCommand ("A00000009 STATUS \"[Gmail]/All Mail\" (MESSAGES RECENT UIDNEXT UIDVALIDITY UNSEEN HIGHESTMODSEQ SIZE MAILBOXID)\r\n", "gmail.status-all-mail.txt"), + new ImapReplayCommand ("A00000010 STATUS \"[Gmail]/Drafts\" (MESSAGES RECENT UIDNEXT UIDVALIDITY UNSEEN HIGHESTMODSEQ SIZE MAILBOXID)\r\n", "gmail.status-drafts.txt"), + new ImapReplayCommand ("A00000011 STATUS \"[Gmail]/Important\" (MESSAGES RECENT UIDNEXT UIDVALIDITY UNSEEN HIGHESTMODSEQ SIZE MAILBOXID)\r\n", "gmail.status-important.txt"), + new ImapReplayCommand ("A00000012 STATUS \"[Gmail]/Sent Mail\" (MESSAGES RECENT UIDNEXT UIDVALIDITY UNSEEN HIGHESTMODSEQ SIZE MAILBOXID)\r\n", "gmail.status-sent-mail.txt"), + new ImapReplayCommand ("A00000013 STATUS \"[Gmail]/Spam\" (MESSAGES RECENT UIDNEXT UIDVALIDITY UNSEEN HIGHESTMODSEQ SIZE MAILBOXID)\r\n", "gmail.status-spam.txt"), + new ImapReplayCommand ("A00000014 STATUS \"[Gmail]/Starred\" (MESSAGES RECENT UIDNEXT UIDVALIDITY UNSEEN HIGHESTMODSEQ SIZE MAILBOXID)\r\n", "gmail.status-starred.txt"), + new ImapReplayCommand ("A00000015 STATUS \"[Gmail]/Trash\" (MESSAGES RECENT UIDNEXT UIDVALIDITY UNSEEN HIGHESTMODSEQ SIZE MAILBOXID)\r\n", "gmail.status-trash.txt"), + new ImapReplayCommand ("A00000016 LSUB \"\" \"*\"\r\n", "gmail.lsub-all.txt"), + new ImapReplayCommand ("A00000017 STATUS INBOX (MESSAGES RECENT UIDNEXT UIDVALIDITY UNSEEN HIGHESTMODSEQ SIZE MAILBOXID)\r\n", "gmail.status-inbox.txt"), + new ImapReplayCommand ("A00000018 STATUS +Folder (MESSAGES RECENT UIDNEXT UIDVALIDITY UNSEEN HIGHESTMODSEQ SIZE MAILBOXID)\r\n", "gmail.status-+folder.txt"), + new ImapReplayCommand ("A00000019 STATUS \"[Gmail]/All Mail\" (MESSAGES RECENT UIDNEXT UIDVALIDITY UNSEEN HIGHESTMODSEQ SIZE MAILBOXID)\r\n", "gmail.status-all-mail.txt"), + new ImapReplayCommand ("A00000020 STATUS \"[Gmail]/Drafts\" (MESSAGES RECENT UIDNEXT UIDVALIDITY UNSEEN HIGHESTMODSEQ SIZE MAILBOXID)\r\n", "gmail.status-drafts.txt"), + new ImapReplayCommand ("A00000021 STATUS \"[Gmail]/Important\" (MESSAGES RECENT UIDNEXT UIDVALIDITY UNSEEN HIGHESTMODSEQ SIZE MAILBOXID)\r\n", "gmail.status-important.txt"), + new ImapReplayCommand ("A00000022 STATUS \"[Gmail]/Sent Mail\" (MESSAGES RECENT UIDNEXT UIDVALIDITY UNSEEN HIGHESTMODSEQ SIZE MAILBOXID)\r\n", "gmail.status-sent-mail.txt"), + new ImapReplayCommand ("A00000023 STATUS \"[Gmail]/Spam\" (MESSAGES RECENT UIDNEXT UIDVALIDITY UNSEEN HIGHESTMODSEQ SIZE MAILBOXID)\r\n", "gmail.status-spam.txt"), + new ImapReplayCommand ("A00000024 STATUS \"[Gmail]/Starred\" (MESSAGES RECENT UIDNEXT UIDVALIDITY UNSEEN HIGHESTMODSEQ SIZE MAILBOXID)\r\n", "gmail.status-starred.txt"), + new ImapReplayCommand ("A00000025 STATUS \"[Gmail]/Trash\" (MESSAGES RECENT UIDNEXT UIDVALIDITY UNSEEN HIGHESTMODSEQ SIZE MAILBOXID)\r\n", "gmail.status-trash.txt"), + new ImapReplayCommand ("A00000026 LOGOUT\r\n", "gmail.logout.txt") + }; + } + + [Test] + public void TestGetFolders () + { + var commands = CreateGetFoldersCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + + Assert.That (client.Capabilities, Is.EqualTo (GMailInitialCapabilities)); + Assert.That (client.AuthenticationMechanisms, Has.Count.EqualTo (5)); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH"), "Expected SASL XOAUTH auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH2"), "Expected SASL XOAUTH2 auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("OAUTHBEARER"), "Expected SASL OAUTHBEARER auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN"), "Expected SASL PLAIN auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN-CLIENTTOKEN"), "Expected SASL PLAIN-CLIENTTOKEN auth mechanism"); + + // Note: Do not try XOAUTH2 + client.AuthenticationMechanisms.Remove ("XOAUTH2"); + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities, Is.EqualTo (GMailAuthenticatedCapabilities | ImapCapabilities.StatusSize | ImapCapabilities.ObjectID)); + + var all = StatusItems.Count | StatusItems.HighestModSeq | StatusItems.Recent | StatusItems.UidNext | StatusItems.UidValidity | StatusItems.Unread | StatusItems.Size | StatusItems.MailboxId; + var folders = client.GetFolders (client.PersonalNamespaces[0], all, true); + Assert.That (folders, Has.Count.EqualTo (10), "Unexpected folder count."); + + AssertFolder (folders[0], "INBOX", "d0f3b017-d3ec-40aa-9bb9-66c1aeccbb24", FolderAttributes.HasNoChildren | FolderAttributes.Inbox, true, 41234, 60, 0, 410, 1, 0, 1024); + AssertFolder (folders[1], "+Folder", "f001Ed6c-ebee-41a5-a65e-9498d3e0aec0", FolderAttributes.HasNoChildren, true, 41234, 6, 0, 7, 1, 0, 1024); + AssertFolder (folders[2], "[Gmail]", null, FolderAttributes.HasChildren | FolderAttributes.NonExistent, true, 0, 0, 0, 0, 0, 0, 0); + AssertFolder (folders[3], "[Gmail]/All Mail", "f668b57d-9f42-453b-b315-a18cd3eb0f85", FolderAttributes.HasNoChildren | FolderAttributes.All, true, 41234, 67, 0, 1210, 11, 3, 1024); + AssertFolder (folders[4], "[Gmail]/Drafts", "fdacc3c7-4e20-4ca0-a0d7-4f7267187e48", FolderAttributes.HasNoChildren | FolderAttributes.Drafts, true, 41234, 0, 0, 1, 6, 0, 1024); + AssertFolder (folders[5], "[Gmail]/Important", "2a0410e1-252a-4ee8-b48d-30111cda734a", FolderAttributes.HasNoChildren | FolderAttributes.Important, true, 41234, 58, 0, 307, 9, 0, 1024); + AssertFolder (folders[6], "[Gmail]/Sent Mail", "79da5ecd-afe4-440e-81ce-64ace69c9fbd", FolderAttributes.HasNoChildren | FolderAttributes.Sent, true, 41234, 4, 0, 7, 5, 0, 1024); + AssertFolder (folders[7], "[Gmail]/Spam", "f5df5af8-5e11-49a5-891d-c3e05591265e", FolderAttributes.HasNoChildren | FolderAttributes.Junk, true, 41234, 0, 0, 1, 3, 0, 1024); + AssertFolder (folders[8], "[Gmail]/Starred", "93ad849a-2127-4c8e-ac41-594cd0a346a4", FolderAttributes.HasNoChildren | FolderAttributes.Flagged, true, 41234, 1, 0, 7, 4, 0, 1024); + AssertFolder (folders[9], "[Gmail]/Trash", "a663f6ce-4f36-434e-9f0c-7f757046a6d4", FolderAttributes.HasNoChildren | FolderAttributes.Trash, true, 41234, 0, 0, 1143, 2, 0, 1024); + + AssertFolder (client.Inbox, "INBOX", "d0f3b017-d3ec-40aa-9bb9-66c1aeccbb24", FolderAttributes.HasNoChildren | FolderAttributes.Inbox, true, 41234, 60, 0, 410, 1, 0, 1024); + AssertFolder (client.GetFolder (SpecialFolder.All), "[Gmail]/All Mail", "f668b57d-9f42-453b-b315-a18cd3eb0f85", FolderAttributes.HasNoChildren | FolderAttributes.All, true, 41234, 67, 0, 1210, 11, 3, 1024); + AssertFolder (client.GetFolder (SpecialFolder.Drafts), "[Gmail]/Drafts", "fdacc3c7-4e20-4ca0-a0d7-4f7267187e48", FolderAttributes.HasNoChildren | FolderAttributes.Drafts, true, 41234, 0, 0, 1, 6, 0, 1024); + AssertFolder (client.GetFolder (SpecialFolder.Important), "[Gmail]/Important", "2a0410e1-252a-4ee8-b48d-30111cda734a", FolderAttributes.HasNoChildren | FolderAttributes.Important, true, 41234, 58, 0, 307, 9, 0, 1024); + AssertFolder (client.GetFolder (SpecialFolder.Sent), "[Gmail]/Sent Mail", "79da5ecd-afe4-440e-81ce-64ace69c9fbd", FolderAttributes.HasNoChildren | FolderAttributes.Sent, true, 41234, 4, 0, 7, 5, 0, 1024); + AssertFolder (client.GetFolder (SpecialFolder.Junk), "[Gmail]/Spam", "f5df5af8-5e11-49a5-891d-c3e05591265e", FolderAttributes.HasNoChildren | FolderAttributes.Junk, true, 41234, 0, 0, 1, 3, 0, 1024); + AssertFolder (client.GetFolder (SpecialFolder.Flagged), "[Gmail]/Starred", "93ad849a-2127-4c8e-ac41-594cd0a346a4", FolderAttributes.HasNoChildren | FolderAttributes.Flagged, true, 41234, 1, 0, 7, 4, 0, 1024); + AssertFolder (client.GetFolder (SpecialFolder.Trash), "[Gmail]/Trash", "a663f6ce-4f36-434e-9f0c-7f757046a6d4", FolderAttributes.HasNoChildren | FolderAttributes.Trash, true, 41234, 0, 0, 1143, 2, 0, 1024); + + // Now make the same query but disable LIST-STATUS + client.Capabilities &= ~ImapCapabilities.ListStatus; + folders = client.GetFolders (client.PersonalNamespaces[0], all, false); + Assert.That (folders, Has.Count.EqualTo (10), "Unexpected folder count."); + + AssertFolder (folders[0], "INBOX", "d0f3b017-d3ec-40aa-9bb9-66c1aeccbb24", FolderAttributes.HasNoChildren | FolderAttributes.Inbox, true, 41234, 60, 0, 410, 1, 0, 1024); + AssertFolder (folders[1], "+Folder", "f001Ed6c-ebee-41a5-a65e-9498d3e0aec0", FolderAttributes.HasNoChildren, true, 41234, 6, 0, 7, 1, 0, 1024); + AssertFolder (folders[2], "[Gmail]", null, FolderAttributes.HasChildren | FolderAttributes.NonExistent, true, 0, 0, 0, 0, 0, 0, 0); + AssertFolder (folders[3], "[Gmail]/All Mail", "f668b57d-9f42-453b-b315-a18cd3eb0f85", FolderAttributes.HasNoChildren | FolderAttributes.All, true, 41234, 67, 0, 1210, 11, 3, 1024); + AssertFolder (folders[4], "[Gmail]/Drafts", "fdacc3c7-4e20-4ca0-a0d7-4f7267187e48", FolderAttributes.HasNoChildren | FolderAttributes.Drafts, true, 41234, 0, 0, 1, 6, 0, 1024); + AssertFolder (folders[5], "[Gmail]/Important", "2a0410e1-252a-4ee8-b48d-30111cda734a", FolderAttributes.HasNoChildren | FolderAttributes.Important, true, 41234, 58, 0, 307, 9, 0, 1024); + AssertFolder (folders[6], "[Gmail]/Sent Mail", "79da5ecd-afe4-440e-81ce-64ace69c9fbd", FolderAttributes.HasNoChildren | FolderAttributes.Sent, true, 41234, 4, 0, 7, 5, 0, 1024); + AssertFolder (folders[7], "[Gmail]/Spam", "f5df5af8-5e11-49a5-891d-c3e05591265e", FolderAttributes.HasNoChildren | FolderAttributes.Junk, true, 41234, 0, 0, 1, 3, 0, 1024); + AssertFolder (folders[8], "[Gmail]/Starred", "93ad849a-2127-4c8e-ac41-594cd0a346a4", FolderAttributes.HasNoChildren | FolderAttributes.Flagged, true, 41234, 1, 0, 7, 4, 0, 1024); + AssertFolder (folders[9], "[Gmail]/Trash", "a663f6ce-4f36-434e-9f0c-7f757046a6d4", FolderAttributes.HasNoChildren | FolderAttributes.Trash, true, 41234, 0, 0, 1143, 2, 0, 1024); + + AssertFolder (client.Inbox, "INBOX", "d0f3b017-d3ec-40aa-9bb9-66c1aeccbb24", FolderAttributes.HasNoChildren | FolderAttributes.Inbox, true, 41234, 60, 0, 410, 1, 0, 1024); + AssertFolder (client.GetFolder (SpecialFolder.All), "[Gmail]/All Mail", "f668b57d-9f42-453b-b315-a18cd3eb0f85", FolderAttributes.HasNoChildren | FolderAttributes.All, true, 41234, 67, 0, 1210, 11, 3, 1024); + AssertFolder (client.GetFolder (SpecialFolder.Drafts), "[Gmail]/Drafts", "fdacc3c7-4e20-4ca0-a0d7-4f7267187e48", FolderAttributes.HasNoChildren | FolderAttributes.Drafts, true, 41234, 0, 0, 1, 6, 0, 1024); + AssertFolder (client.GetFolder (SpecialFolder.Important), "[Gmail]/Important", "2a0410e1-252a-4ee8-b48d-30111cda734a", FolderAttributes.HasNoChildren | FolderAttributes.Important, true, 41234, 58, 0, 307, 9, 0, 1024); + AssertFolder (client.GetFolder (SpecialFolder.Sent), "[Gmail]/Sent Mail", "79da5ecd-afe4-440e-81ce-64ace69c9fbd", FolderAttributes.HasNoChildren | FolderAttributes.Sent, true, 41234, 4, 0, 7, 5, 0, 1024); + AssertFolder (client.GetFolder (SpecialFolder.Junk), "[Gmail]/Spam", "f5df5af8-5e11-49a5-891d-c3e05591265e", FolderAttributes.HasNoChildren | FolderAttributes.Junk, true, 41234, 0, 0, 1, 3, 0, 1024); + AssertFolder (client.GetFolder (SpecialFolder.Flagged), "[Gmail]/Starred", "93ad849a-2127-4c8e-ac41-594cd0a346a4", FolderAttributes.HasNoChildren | FolderAttributes.Flagged, true, 41234, 1, 0, 7, 4, 0, 1024); + AssertFolder (client.GetFolder (SpecialFolder.Trash), "[Gmail]/Trash", "a663f6ce-4f36-434e-9f0c-7f757046a6d4", FolderAttributes.HasNoChildren | FolderAttributes.Trash, true, 41234, 0, 0, 1143, 2, 0, 1024); + + // Now make the same query but disable LIST-EXTENDED + client.Capabilities &= ~ImapCapabilities.ListExtended; + folders = client.GetFolders (client.PersonalNamespaces[0], all, true); + Assert.That (folders, Has.Count.EqualTo (10), "Unexpected folder count."); + + AssertFolder (folders[0], "INBOX", "d0f3b017-d3ec-40aa-9bb9-66c1aeccbb24", FolderAttributes.HasNoChildren | FolderAttributes.Inbox, true, 41234, 60, 0, 410, 1, 0, 1024); + AssertFolder (folders[1], "+Folder", "f001Ed6c-ebee-41a5-a65e-9498d3e0aec0", FolderAttributes.HasNoChildren, true, 41234, 6, 0, 7, 1, 0, 1024); + AssertFolder (folders[2], "[Gmail]", null, FolderAttributes.HasChildren | FolderAttributes.NonExistent, true, 0, 0, 0, 0, 0, 0, 0); + AssertFolder (folders[3], "[Gmail]/All Mail", "f668b57d-9f42-453b-b315-a18cd3eb0f85", FolderAttributes.HasNoChildren | FolderAttributes.All, true, 41234, 67, 0, 1210, 11, 3, 1024); + AssertFolder (folders[4], "[Gmail]/Drafts", "fdacc3c7-4e20-4ca0-a0d7-4f7267187e48", FolderAttributes.HasNoChildren | FolderAttributes.Drafts, true, 41234, 0, 0, 1, 6, 0, 1024); + AssertFolder (folders[5], "[Gmail]/Important", "2a0410e1-252a-4ee8-b48d-30111cda734a", FolderAttributes.HasNoChildren | FolderAttributes.Important, true, 41234, 58, 0, 307, 9, 0, 1024); + AssertFolder (folders[6], "[Gmail]/Sent Mail", "79da5ecd-afe4-440e-81ce-64ace69c9fbd", FolderAttributes.HasNoChildren | FolderAttributes.Sent, true, 41234, 4, 0, 7, 5, 0, 1024); + AssertFolder (folders[7], "[Gmail]/Spam", "f5df5af8-5e11-49a5-891d-c3e05591265e", FolderAttributes.HasNoChildren | FolderAttributes.Junk, true, 41234, 0, 0, 1, 3, 0, 1024); + AssertFolder (folders[8], "[Gmail]/Starred", "93ad849a-2127-4c8e-ac41-594cd0a346a4", FolderAttributes.HasNoChildren | FolderAttributes.Flagged, true, 41234, 1, 0, 7, 4, 0, 1024); + AssertFolder (folders[9], "[Gmail]/Trash", "a663f6ce-4f36-434e-9f0c-7f757046a6d4", FolderAttributes.HasNoChildren | FolderAttributes.Trash, true, 41234, 0, 0, 1143, 2, 0, 1024); + + AssertFolder (client.Inbox, "INBOX", "d0f3b017-d3ec-40aa-9bb9-66c1aeccbb24", FolderAttributes.HasNoChildren | FolderAttributes.Inbox, true, 41234, 60, 0, 410, 1, 0, 1024); + AssertFolder (client.GetFolder (SpecialFolder.All), "[Gmail]/All Mail", "f668b57d-9f42-453b-b315-a18cd3eb0f85", FolderAttributes.HasNoChildren | FolderAttributes.All, true, 41234, 67, 0, 1210, 11, 3, 1024); + AssertFolder (client.GetFolder (SpecialFolder.Drafts), "[Gmail]/Drafts", "fdacc3c7-4e20-4ca0-a0d7-4f7267187e48", FolderAttributes.HasNoChildren | FolderAttributes.Drafts, true, 41234, 0, 0, 1, 6, 0, 1024); + AssertFolder (client.GetFolder (SpecialFolder.Important), "[Gmail]/Important", "2a0410e1-252a-4ee8-b48d-30111cda734a", FolderAttributes.HasNoChildren | FolderAttributes.Important, true, 41234, 58, 0, 307, 9, 0, 1024); + AssertFolder (client.GetFolder (SpecialFolder.Sent), "[Gmail]/Sent Mail", "79da5ecd-afe4-440e-81ce-64ace69c9fbd", FolderAttributes.HasNoChildren | FolderAttributes.Sent, true, 41234, 4, 0, 7, 5, 0, 1024); + AssertFolder (client.GetFolder (SpecialFolder.Junk), "[Gmail]/Spam", "f5df5af8-5e11-49a5-891d-c3e05591265e", FolderAttributes.HasNoChildren | FolderAttributes.Junk, true, 41234, 0, 0, 1, 3, 0, 1024); + AssertFolder (client.GetFolder (SpecialFolder.Flagged), "[Gmail]/Starred", "93ad849a-2127-4c8e-ac41-594cd0a346a4", FolderAttributes.HasNoChildren | FolderAttributes.Flagged, true, 41234, 1, 0, 7, 4, 0, 1024); + AssertFolder (client.GetFolder (SpecialFolder.Trash), "[Gmail]/Trash", "a663f6ce-4f36-434e-9f0c-7f757046a6d4", FolderAttributes.HasNoChildren | FolderAttributes.Trash, true, 41234, 0, 0, 1143, 2, 0, 1024); + + client.Disconnect (true); + } + } + + [Test] + public async Task TestGetFoldersAsync () + { + var commands = CreateGetFoldersCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + + Assert.That (client.Capabilities, Is.EqualTo (GMailInitialCapabilities)); + Assert.That (client.AuthenticationMechanisms, Has.Count.EqualTo (5)); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH"), "Expected SASL XOAUTH auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH2"), "Expected SASL XOAUTH2 auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("OAUTHBEARER"), "Expected SASL OAUTHBEARER auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN"), "Expected SASL PLAIN auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN-CLIENTTOKEN"), "Expected SASL PLAIN-CLIENTTOKEN auth mechanism"); + + // Note: Do not try XOAUTH2 + client.AuthenticationMechanisms.Remove ("XOAUTH2"); + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities, Is.EqualTo (GMailAuthenticatedCapabilities | ImapCapabilities.StatusSize | ImapCapabilities.ObjectID)); + + var all = StatusItems.Count | StatusItems.HighestModSeq | StatusItems.Recent | StatusItems.UidNext | StatusItems.UidValidity | StatusItems.Unread | StatusItems.Size | StatusItems.MailboxId; + var folders = await client.GetFoldersAsync (client.PersonalNamespaces[0], all, true); + Assert.That (folders, Has.Count.EqualTo (10), "Unexpected folder count."); + + AssertFolder (folders[0], "INBOX", "d0f3b017-d3ec-40aa-9bb9-66c1aeccbb24", FolderAttributes.HasNoChildren | FolderAttributes.Inbox, true, 41234, 60, 0, 410, 1, 0, 1024); + AssertFolder (folders[1], "+Folder", "f001Ed6c-ebee-41a5-a65e-9498d3e0aec0", FolderAttributes.HasNoChildren, true, 41234, 6, 0, 7, 1, 0, 1024); + AssertFolder (folders[2], "[Gmail]", null, FolderAttributes.HasChildren | FolderAttributes.NonExistent, true, 0, 0, 0, 0, 0, 0, 0); + AssertFolder (folders[3], "[Gmail]/All Mail", "f668b57d-9f42-453b-b315-a18cd3eb0f85", FolderAttributes.HasNoChildren | FolderAttributes.All, true, 41234, 67, 0, 1210, 11, 3, 1024); + AssertFolder (folders[4], "[Gmail]/Drafts", "fdacc3c7-4e20-4ca0-a0d7-4f7267187e48", FolderAttributes.HasNoChildren | FolderAttributes.Drafts, true, 41234, 0, 0, 1, 6, 0, 1024); + AssertFolder (folders[5], "[Gmail]/Important", "2a0410e1-252a-4ee8-b48d-30111cda734a", FolderAttributes.HasNoChildren | FolderAttributes.Important, true, 41234, 58, 0, 307, 9, 0, 1024); + AssertFolder (folders[6], "[Gmail]/Sent Mail", "79da5ecd-afe4-440e-81ce-64ace69c9fbd", FolderAttributes.HasNoChildren | FolderAttributes.Sent, true, 41234, 4, 0, 7, 5, 0, 1024); + AssertFolder (folders[7], "[Gmail]/Spam", "f5df5af8-5e11-49a5-891d-c3e05591265e", FolderAttributes.HasNoChildren | FolderAttributes.Junk, true, 41234, 0, 0, 1, 3, 0, 1024); + AssertFolder (folders[8], "[Gmail]/Starred", "93ad849a-2127-4c8e-ac41-594cd0a346a4", FolderAttributes.HasNoChildren | FolderAttributes.Flagged, true, 41234, 1, 0, 7, 4, 0, 1024); + AssertFolder (folders[9], "[Gmail]/Trash", "a663f6ce-4f36-434e-9f0c-7f757046a6d4", FolderAttributes.HasNoChildren | FolderAttributes.Trash, true, 41234, 0, 0, 1143, 2, 0, 1024); + + AssertFolder (client.Inbox, "INBOX", "d0f3b017-d3ec-40aa-9bb9-66c1aeccbb24", FolderAttributes.HasNoChildren | FolderAttributes.Inbox, true, 41234, 60, 0, 410, 1, 0, 1024); + AssertFolder (client.GetFolder (SpecialFolder.All), "[Gmail]/All Mail", "f668b57d-9f42-453b-b315-a18cd3eb0f85", FolderAttributes.HasNoChildren | FolderAttributes.All, true, 41234, 67, 0, 1210, 11, 3, 1024); + AssertFolder (client.GetFolder (SpecialFolder.Drafts), "[Gmail]/Drafts", "fdacc3c7-4e20-4ca0-a0d7-4f7267187e48", FolderAttributes.HasNoChildren | FolderAttributes.Drafts, true, 41234, 0, 0, 1, 6, 0, 1024); + AssertFolder (client.GetFolder (SpecialFolder.Important), "[Gmail]/Important", "2a0410e1-252a-4ee8-b48d-30111cda734a", FolderAttributes.HasNoChildren | FolderAttributes.Important, true, 41234, 58, 0, 307, 9, 0, 1024); + AssertFolder (client.GetFolder (SpecialFolder.Sent), "[Gmail]/Sent Mail", "79da5ecd-afe4-440e-81ce-64ace69c9fbd", FolderAttributes.HasNoChildren | FolderAttributes.Sent, true, 41234, 4, 0, 7, 5, 0, 1024); + AssertFolder (client.GetFolder (SpecialFolder.Junk), "[Gmail]/Spam", "f5df5af8-5e11-49a5-891d-c3e05591265e", FolderAttributes.HasNoChildren | FolderAttributes.Junk, true, 41234, 0, 0, 1, 3, 0, 1024); + AssertFolder (client.GetFolder (SpecialFolder.Flagged), "[Gmail]/Starred", "93ad849a-2127-4c8e-ac41-594cd0a346a4", FolderAttributes.HasNoChildren | FolderAttributes.Flagged, true, 41234, 1, 0, 7, 4, 0, 1024); + AssertFolder (client.GetFolder (SpecialFolder.Trash), "[Gmail]/Trash", "a663f6ce-4f36-434e-9f0c-7f757046a6d4", FolderAttributes.HasNoChildren | FolderAttributes.Trash, true, 41234, 0, 0, 1143, 2, 0, 1024); + + // Now make the same query but disable LIST-STATUS + client.Capabilities &= ~ImapCapabilities.ListStatus; + folders = await client.GetFoldersAsync (client.PersonalNamespaces[0], all, false); + Assert.That (folders, Has.Count.EqualTo (10), "Unexpected folder count."); + + AssertFolder (folders[0], "INBOX", "d0f3b017-d3ec-40aa-9bb9-66c1aeccbb24", FolderAttributes.HasNoChildren | FolderAttributes.Inbox, true, 41234, 60, 0, 410, 1, 0, 1024); + AssertFolder (folders[1], "+Folder", "f001Ed6c-ebee-41a5-a65e-9498d3e0aec0", FolderAttributes.HasNoChildren, true, 41234, 6, 0, 7, 1, 0, 1024); + AssertFolder (folders[2], "[Gmail]", null, FolderAttributes.HasChildren | FolderAttributes.NonExistent, true, 0, 0, 0, 0, 0, 0, 0); + AssertFolder (folders[3], "[Gmail]/All Mail", "f668b57d-9f42-453b-b315-a18cd3eb0f85", FolderAttributes.HasNoChildren | FolderAttributes.All, true, 41234, 67, 0, 1210, 11, 3, 1024); + AssertFolder (folders[4], "[Gmail]/Drafts", "fdacc3c7-4e20-4ca0-a0d7-4f7267187e48", FolderAttributes.HasNoChildren | FolderAttributes.Drafts, true, 41234, 0, 0, 1, 6, 0, 1024); + AssertFolder (folders[5], "[Gmail]/Important", "2a0410e1-252a-4ee8-b48d-30111cda734a", FolderAttributes.HasNoChildren | FolderAttributes.Important, true, 41234, 58, 0, 307, 9, 0, 1024); + AssertFolder (folders[6], "[Gmail]/Sent Mail", "79da5ecd-afe4-440e-81ce-64ace69c9fbd", FolderAttributes.HasNoChildren | FolderAttributes.Sent, true, 41234, 4, 0, 7, 5, 0, 1024); + AssertFolder (folders[7], "[Gmail]/Spam", "f5df5af8-5e11-49a5-891d-c3e05591265e", FolderAttributes.HasNoChildren | FolderAttributes.Junk, true, 41234, 0, 0, 1, 3, 0, 1024); + AssertFolder (folders[8], "[Gmail]/Starred", "93ad849a-2127-4c8e-ac41-594cd0a346a4", FolderAttributes.HasNoChildren | FolderAttributes.Flagged, true, 41234, 1, 0, 7, 4, 0, 1024); + AssertFolder (folders[9], "[Gmail]/Trash", "a663f6ce-4f36-434e-9f0c-7f757046a6d4", FolderAttributes.HasNoChildren | FolderAttributes.Trash, true, 41234, 0, 0, 1143, 2, 0, 1024); + + AssertFolder (client.Inbox, "INBOX", "d0f3b017-d3ec-40aa-9bb9-66c1aeccbb24", FolderAttributes.HasNoChildren | FolderAttributes.Inbox, true, 41234, 60, 0, 410, 1, 0, 1024); + AssertFolder (client.GetFolder (SpecialFolder.All), "[Gmail]/All Mail", "f668b57d-9f42-453b-b315-a18cd3eb0f85", FolderAttributes.HasNoChildren | FolderAttributes.All, true, 41234, 67, 0, 1210, 11, 3, 1024); + AssertFolder (client.GetFolder (SpecialFolder.Drafts), "[Gmail]/Drafts", "fdacc3c7-4e20-4ca0-a0d7-4f7267187e48", FolderAttributes.HasNoChildren | FolderAttributes.Drafts, true, 41234, 0, 0, 1, 6, 0, 1024); + AssertFolder (client.GetFolder (SpecialFolder.Important), "[Gmail]/Important", "2a0410e1-252a-4ee8-b48d-30111cda734a", FolderAttributes.HasNoChildren | FolderAttributes.Important, true, 41234, 58, 0, 307, 9, 0, 1024); + AssertFolder (client.GetFolder (SpecialFolder.Sent), "[Gmail]/Sent Mail", "79da5ecd-afe4-440e-81ce-64ace69c9fbd", FolderAttributes.HasNoChildren | FolderAttributes.Sent, true, 41234, 4, 0, 7, 5, 0, 1024); + AssertFolder (client.GetFolder (SpecialFolder.Junk), "[Gmail]/Spam", "f5df5af8-5e11-49a5-891d-c3e05591265e", FolderAttributes.HasNoChildren | FolderAttributes.Junk, true, 41234, 0, 0, 1, 3, 0, 1024); + AssertFolder (client.GetFolder (SpecialFolder.Flagged), "[Gmail]/Starred", "93ad849a-2127-4c8e-ac41-594cd0a346a4", FolderAttributes.HasNoChildren | FolderAttributes.Flagged, true, 41234, 1, 0, 7, 4, 0, 1024); + AssertFolder (client.GetFolder (SpecialFolder.Trash), "[Gmail]/Trash", "a663f6ce-4f36-434e-9f0c-7f757046a6d4", FolderAttributes.HasNoChildren | FolderAttributes.Trash, true, 41234, 0, 0, 1143, 2, 0, 1024); + + // Now make the same query but disable LIST-STATUS + client.Capabilities &= ~ImapCapabilities.ListExtended; + folders = await client.GetFoldersAsync (client.PersonalNamespaces[0], all, true); + Assert.That (folders, Has.Count.EqualTo (10), "Unexpected folder count."); + + AssertFolder (folders[0], "INBOX", "d0f3b017-d3ec-40aa-9bb9-66c1aeccbb24", FolderAttributes.HasNoChildren | FolderAttributes.Inbox, true, 41234, 60, 0, 410, 1, 0, 1024); + AssertFolder (folders[1], "+Folder", "f001Ed6c-ebee-41a5-a65e-9498d3e0aec0", FolderAttributes.HasNoChildren, true, 41234, 6, 0, 7, 1, 0, 1024); + AssertFolder (folders[2], "[Gmail]", null, FolderAttributes.HasChildren | FolderAttributes.NonExistent, true, 0, 0, 0, 0, 0, 0, 0); + AssertFolder (folders[3], "[Gmail]/All Mail", "f668b57d-9f42-453b-b315-a18cd3eb0f85", FolderAttributes.HasNoChildren | FolderAttributes.All, true, 41234, 67, 0, 1210, 11, 3, 1024); + AssertFolder (folders[4], "[Gmail]/Drafts", "fdacc3c7-4e20-4ca0-a0d7-4f7267187e48", FolderAttributes.HasNoChildren | FolderAttributes.Drafts, true, 41234, 0, 0, 1, 6, 0, 1024); + AssertFolder (folders[5], "[Gmail]/Important", "2a0410e1-252a-4ee8-b48d-30111cda734a", FolderAttributes.HasNoChildren | FolderAttributes.Important, true, 41234, 58, 0, 307, 9, 0, 1024); + AssertFolder (folders[6], "[Gmail]/Sent Mail", "79da5ecd-afe4-440e-81ce-64ace69c9fbd", FolderAttributes.HasNoChildren | FolderAttributes.Sent, true, 41234, 4, 0, 7, 5, 0, 1024); + AssertFolder (folders[7], "[Gmail]/Spam", "f5df5af8-5e11-49a5-891d-c3e05591265e", FolderAttributes.HasNoChildren | FolderAttributes.Junk, true, 41234, 0, 0, 1, 3, 0, 1024); + AssertFolder (folders[8], "[Gmail]/Starred", "93ad849a-2127-4c8e-ac41-594cd0a346a4", FolderAttributes.HasNoChildren | FolderAttributes.Flagged, true, 41234, 1, 0, 7, 4, 0, 1024); + AssertFolder (folders[9], "[Gmail]/Trash", "a663f6ce-4f36-434e-9f0c-7f757046a6d4", FolderAttributes.HasNoChildren | FolderAttributes.Trash, true, 41234, 0, 0, 1143, 2, 0, 1024); + + AssertFolder (client.Inbox, "INBOX", "d0f3b017-d3ec-40aa-9bb9-66c1aeccbb24", FolderAttributes.HasNoChildren | FolderAttributes.Inbox, true, 41234, 60, 0, 410, 1, 0, 1024); + AssertFolder (client.GetFolder (SpecialFolder.All), "[Gmail]/All Mail", "f668b57d-9f42-453b-b315-a18cd3eb0f85", FolderAttributes.HasNoChildren | FolderAttributes.All, true, 41234, 67, 0, 1210, 11, 3, 1024); + AssertFolder (client.GetFolder (SpecialFolder.Drafts), "[Gmail]/Drafts", "fdacc3c7-4e20-4ca0-a0d7-4f7267187e48", FolderAttributes.HasNoChildren | FolderAttributes.Drafts, true, 41234, 0, 0, 1, 6, 0, 1024); + AssertFolder (client.GetFolder (SpecialFolder.Important), "[Gmail]/Important", "2a0410e1-252a-4ee8-b48d-30111cda734a", FolderAttributes.HasNoChildren | FolderAttributes.Important, true, 41234, 58, 0, 307, 9, 0, 1024); + AssertFolder (client.GetFolder (SpecialFolder.Sent), "[Gmail]/Sent Mail", "79da5ecd-afe4-440e-81ce-64ace69c9fbd", FolderAttributes.HasNoChildren | FolderAttributes.Sent, true, 41234, 4, 0, 7, 5, 0, 1024); + AssertFolder (client.GetFolder (SpecialFolder.Junk), "[Gmail]/Spam", "f5df5af8-5e11-49a5-891d-c3e05591265e", FolderAttributes.HasNoChildren | FolderAttributes.Junk, true, 41234, 0, 0, 1, 3, 0, 1024); + AssertFolder (client.GetFolder (SpecialFolder.Flagged), "[Gmail]/Starred", "93ad849a-2127-4c8e-ac41-594cd0a346a4", FolderAttributes.HasNoChildren | FolderAttributes.Flagged, true, 41234, 1, 0, 7, 4, 0, 1024); + AssertFolder (client.GetFolder (SpecialFolder.Trash), "[Gmail]/Trash", "a663f6ce-4f36-434e-9f0c-7f757046a6d4", FolderAttributes.HasNoChildren | FolderAttributes.Trash, true, 41234, 0, 0, 1143, 2, 0, 1024); + + await client.DisconnectAsync (true); + } + } + + static List CreateGetQuotaNonexistentQuotaRootCommands () + { + return new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt"), + new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "gmail.authenticate.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-inbox.txt"), + new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt"), + new ImapReplayCommand ("A00000005 GETQUOTAROOT INBOX\r\n", "common.getquota-no-root.txt"), + new ImapReplayCommand ("A00000006 LIST \"\" storage=0 RETURN (SUBSCRIBED CHILDREN)\r\n", ImapReplayCommandResponse.OK) + }; + } + + [Test] + public void TestGetQuotaNonexistentQuotaRoot () + { + var commands = CreateGetQuotaNonexistentQuotaRootCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + + Assert.That (client.Capabilities, Is.EqualTo (GMailInitialCapabilities)); + Assert.That (client.AuthenticationMechanisms, Has.Count.EqualTo (5)); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH"), "Expected SASL XOAUTH auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH2"), "Expected SASL XOAUTH2 auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("OAUTHBEARER"), "Expected SASL OAUTHBEARER auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN"), "Expected SASL PLAIN auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN-CLIENTTOKEN"), "Expected SASL PLAIN-CLIENTTOKEN auth mechanism"); + + // Note: Do not try XOAUTH2 + client.AuthenticationMechanisms.Remove ("XOAUTH2"); + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities, Is.EqualTo (GMailAuthenticatedCapabilities)); + + var inbox = client.Inbox; + + Assert.That (inbox, Is.Not.Null, "Expected non-null Inbox folder."); + Assert.That (inbox.Attributes, Is.EqualTo (FolderAttributes.Inbox | FolderAttributes.HasNoChildren | FolderAttributes.Subscribed), "Expected Inbox attributes to be \\HasNoChildren."); + + var quota = inbox.GetQuota (); + Assert.That (quota, Is.Not.Null, "Expected a non-null GETQUOTAROOT response."); + Assert.That (quota.QuotaRoot.Exists, Is.False); + Assert.That (quota.QuotaRoot.FullName, Is.EqualTo ("storage=0")); + Assert.That (quota.CurrentStorageSize.Value, Is.EqualTo (28257)); + Assert.That (quota.StorageLimit.Value, Is.EqualTo (256000)); + Assert.That (quota.CurrentMessageCount.HasValue, Is.False); + Assert.That (quota.MessageLimit.HasValue, Is.False); + + client.Disconnect (false); + } + } + + [Test] + public async Task TestGetQuotaNonexistentQuotaRootAsync () + { + var commands = CreateGetQuotaNonexistentQuotaRootCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + + Assert.That (client.Capabilities, Is.EqualTo (GMailInitialCapabilities)); + Assert.That (client.AuthenticationMechanisms, Has.Count.EqualTo (5)); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH"), "Expected SASL XOAUTH auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH2"), "Expected SASL XOAUTH2 auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("OAUTHBEARER"), "Expected SASL OAUTHBEARER auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN"), "Expected SASL PLAIN auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN-CLIENTTOKEN"), "Expected SASL PLAIN-CLIENTTOKEN auth mechanism"); + + // Note: Do not try XOAUTH2 + client.AuthenticationMechanisms.Remove ("XOAUTH2"); + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities, Is.EqualTo (GMailAuthenticatedCapabilities)); + + var inbox = client.Inbox; + + Assert.That (inbox, Is.Not.Null, "Expected non-null Inbox folder."); + Assert.That (inbox.Attributes, Is.EqualTo (FolderAttributes.Inbox | FolderAttributes.HasNoChildren | FolderAttributes.Subscribed), "Expected Inbox attributes to be \\HasNoChildren."); + + var quota = await inbox.GetQuotaAsync (); + Assert.That (quota, Is.Not.Null, "Expected a non-null GETQUOTAROOT response."); + Assert.That (quota.QuotaRoot.Exists, Is.False); + Assert.That (quota.QuotaRoot.FullName, Is.EqualTo ("storage=0")); + Assert.That (quota.CurrentStorageSize.Value, Is.EqualTo (28257)); + Assert.That (quota.StorageLimit.Value, Is.EqualTo (256000)); + Assert.That (quota.CurrentMessageCount.HasValue, Is.False); + Assert.That (quota.MessageLimit.HasValue, Is.False); + + await client.DisconnectAsync (false); + } + } + + static MimeMessage CreateThreadableMessage (string subject, string msgid, string references, DateTimeOffset date) + { + var message = new MimeMessage (); + message.From.Add (new MailboxAddress ("Unit Tests", "unit-tests@mimekit.net")); + message.To.Add (new MailboxAddress ("Unit Tests", "unit-tests@mimekit.net")); + message.MessageId = msgid; + message.Subject = subject; + message.Date = date; + + if (references != null) { + foreach (var reference in references.Split (' ')) + message.References.Add (reference); + } + + message.Body = new TextPart ("plain") { Text = "This is the message body.\r\n" }; + + return message; + } + + static List CreateDovecotCommands (out List internalDates, out List messages, out List flags) + { + var commands = new List { + new ImapReplayCommand ("", "dovecot.greeting.txt"), + new ImapReplayCommand ("A00000000 LOGIN username password\r\n", "dovecot.authenticate.txt"), + new ImapReplayCommand ("A00000001 NAMESPACE\r\n", "dovecot.namespace.txt"), + new ImapReplayCommand ("A00000002 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-inbox.txt"), + new ImapReplayCommand ("A00000003 LIST (SPECIAL-USE) \"\" \"*\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-special-use.txt"), + new ImapReplayCommand ("A00000004 ENABLE QRESYNC CONDSTORE\r\n", "dovecot.enable-qresync.txt"), + new ImapReplayCommand ("A00000005 LIST \"\" \"%\" RETURN (SUBSCRIBED CHILDREN STATUS (MESSAGES RECENT UIDNEXT UIDVALIDITY UNSEEN HIGHESTMODSEQ))\r\n", "dovecot.list-personal.txt"), + new ImapReplayCommand ("A00000006 CREATE UnitTests.\r\n", ImapReplayCommandResponse.OK), + new ImapReplayCommand ("A00000007 LIST \"\" UnitTests\r\n", "dovecot.list-unittests.txt"), + new ImapReplayCommand ("A00000008 CREATE UnitTests.Messages\r\n", ImapReplayCommandResponse.OK), + new ImapReplayCommand ("A00000009 LIST \"\" UnitTests.Messages\r\n", "dovecot.list-unittests-messages.txt") + }; + + var command = new StringBuilder ("A00000010 APPEND UnitTests.Messages"); + var now = DateTimeOffset.Now; + + internalDates = new List (); + messages = new List (); + flags = new List (); + + messages.Add (CreateThreadableMessage ("A", "", null, now.AddMinutes (-7))); + messages.Add (CreateThreadableMessage ("B", "", "", now.AddMinutes (-6))); + messages.Add (CreateThreadableMessage ("C", "", " ", now.AddMinutes (-5))); + messages.Add (CreateThreadableMessage ("D", "", "", now.AddMinutes (-4))); + messages.Add (CreateThreadableMessage ("E", "", " ", now.AddMinutes (-3))); + messages.Add (CreateThreadableMessage ("F", "", "", now.AddMinutes (-2))); + messages.Add (CreateThreadableMessage ("G", "", null, now.AddMinutes (-1))); + messages.Add (CreateThreadableMessage ("H", "", null, now)); + + for (int i = 0; i < messages.Count; i++) { + var message = messages[i]; + string latin1; + long length; + + internalDates.Add (messages[i].Date); + flags.Add (MessageFlags.Draft); + + using (var stream = new MemoryStream ()) { + var options = FormatOptions.Default.Clone (); + options.NewLineFormat = NewLineFormat.Dos; + + message.WriteTo (options, stream); + length = stream.Length; + stream.Position = 0; + + using (var reader = new StreamReader (stream, TextEncodings.Latin1)) + latin1 = reader.ReadToEnd (); + } + + command.AppendFormat (" (\\Draft) \"{0}\" ", ImapUtils.FormatInternalDate (message.Date)); + command.Append ('{'); + command.AppendFormat ("{0}+", length); + command.Append ("}\r\n"); + command.Append (latin1); + } + command.Append ("\r\n"); + commands.Add (new ImapReplayCommand (command.ToString (), "dovecot.multiappend.txt")); + commands.Add (new ImapReplayCommand ("A00000011 SELECT UnitTests.Messages (CONDSTORE)\r\n", "dovecot.select-unittests-messages.txt")); + commands.Add (new ImapReplayCommand ("A00000012 UID STORE 1:8 +FLAGS.SILENT (\\Seen)\r\n", "dovecot.store-seen.txt")); + commands.Add (new ImapReplayCommand ("A00000013 UID STORE 1:3 +FLAGS.SILENT (\\Answered)\r\n", "dovecot.store-answered.txt")); + commands.Add (new ImapReplayCommand ("A00000014 UID STORE 8 +FLAGS.SILENT (\\Deleted)\r\n", "dovecot.store-deleted.txt")); + commands.Add (new ImapReplayCommand ("A00000015 UID EXPUNGE 8\r\n", "dovecot.uid-expunge.txt")); + commands.Add (new ImapReplayCommand ("A00000016 UID THREAD REFERENCES US-ASCII ALL\r\n", "dovecot.thread-references.txt")); + commands.Add (new ImapReplayCommand ("A00000017 UID THREAD ORDEREDSUBJECT US-ASCII UID 1:* ALL\r\n", "dovecot.thread-orderedsubject.txt")); + commands.Add (new ImapReplayCommand ("A00000018 UNSELECT\r\n", ImapReplayCommandResponse.OK)); + commands.Add (new ImapReplayCommand ("A00000019 SELECT UnitTests.Messages (QRESYNC (1436832084 2 1:8))\r\n", "dovecot.select-unittests-messages-qresync.txt")); + commands.Add (new ImapReplayCommand ("A00000020 UID SEARCH RETURN (ALL RELEVANCY COUNT MIN MAX) MODSEQ 2\r\n", "dovecot.search-changed-since.txt")); + commands.Add (new ImapReplayCommand ("A00000021 UID FETCH 1:7 (UID FLAGS MODSEQ)\r\n", "dovecot.fetch1.txt")); + commands.Add (new ImapReplayCommand ("A00000022 UID FETCH 1:* (UID FLAGS MODSEQ) (CHANGEDSINCE 2 VANISHED)\r\n", "dovecot.fetch2.txt")); + commands.Add (new ImapReplayCommand ("A00000023 UID SORT RETURN (ALL RELEVANCY COUNT MIN MAX) (REVERSE ARRIVAL) US-ASCII ALL\r\n", "dovecot.sort-reverse-arrival.txt")); + commands.Add (new ImapReplayCommand ("A00000024 UID SEARCH RETURN (ALL) UNDELETED SEEN\r\n", "dovecot.optimized-search.txt")); + commands.Add (new ImapReplayCommand ("A00000025 CREATE UnitTests.Destination\r\n", ImapReplayCommandResponse.OK)); + commands.Add (new ImapReplayCommand ("A00000026 LIST \"\" UnitTests.Destination\r\n", "dovecot.list-unittests-destination.txt")); + commands.Add (new ImapReplayCommand ("A00000027 UID COPY 1:7 UnitTests.Destination\r\n", "dovecot.copy.txt")); + commands.Add (new ImapReplayCommand ("A00000028 UID MOVE 1:7 UnitTests.Destination\r\n", "dovecot.move.txt")); + commands.Add (new ImapReplayCommand ("A00000029 STATUS UnitTests.Destination (MESSAGES RECENT UIDNEXT UIDVALIDITY UNSEEN HIGHESTMODSEQ)\r\n", "dovecot.status-unittests-destination.txt")); + commands.Add (new ImapReplayCommand ("A00000030 SELECT UnitTests.Destination (CONDSTORE)\r\n", "dovecot.select-unittests-destination.txt")); + commands.Add (new ImapReplayCommand ("A00000031 UID FETCH 1:* (UID FLAGS INTERNALDATE RFC822.SIZE ENVELOPE BODYSTRUCTURE MODSEQ BODY.PEEK[HEADER.FIELDS (REFERENCES X-MAILER)]) (CHANGEDSINCE 1 VANISHED)\r\n", "dovecot.fetch3.txt")); + commands.Add (new ImapReplayCommand ("A00000032 FETCH 1:* (UID FLAGS INTERNALDATE RFC822.SIZE ENVELOPE BODYSTRUCTURE MODSEQ BODY.PEEK[HEADER.FIELDS (REFERENCES X-MAILER)]) (CHANGEDSINCE 1)\r\n", "dovecot.fetch3.txt")); + commands.Add (new ImapReplayCommand ("A00000033 FETCH 1:14 (UID FLAGS INTERNALDATE RFC822.SIZE ENVELOPE BODYSTRUCTURE MODSEQ BODY.PEEK[HEADER.FIELDS (REFERENCES X-MAILER)]) (CHANGEDSINCE 1)\r\n", "dovecot.fetch3.txt")); + commands.Add (new ImapReplayCommand ("A00000034 FETCH 1:* (UID FLAGS INTERNALDATE RFC822.SIZE ENVELOPE BODYSTRUCTURE MODSEQ BODY.PEEK[HEADER.FIELDS (REFERENCES)]) (CHANGEDSINCE 1)\r\n", "dovecot.fetch4.txt")); + commands.Add (new ImapReplayCommand ("A00000035 FETCH 1:14 (UID FLAGS INTERNALDATE RFC822.SIZE ENVELOPE BODYSTRUCTURE MODSEQ BODY.PEEK[HEADER.FIELDS (REFERENCES)]) (CHANGEDSINCE 1)\r\n", "dovecot.fetch4.txt")); + commands.Add (new ImapReplayCommand ("A00000036 UID FETCH 1:* (UID FLAGS INTERNALDATE RFC822.SIZE ENVELOPE BODYSTRUCTURE MODSEQ BODY.PEEK[HEADER.FIELDS (REFERENCES X-MAILER)])\r\n", "dovecot.fetch3.txt")); + commands.Add (new ImapReplayCommand ("A00000037 FETCH 1:* (UID FLAGS INTERNALDATE RFC822.SIZE ENVELOPE BODYSTRUCTURE MODSEQ BODY.PEEK[HEADER.FIELDS (REFERENCES X-MAILER)])\r\n", "dovecot.fetch3.txt")); + commands.Add (new ImapReplayCommand ("A00000038 FETCH 1:14 (UID FLAGS INTERNALDATE RFC822.SIZE ENVELOPE BODYSTRUCTURE MODSEQ BODY.PEEK[HEADER.FIELDS (REFERENCES X-MAILER)])\r\n", "dovecot.fetch3.txt")); + commands.Add (new ImapReplayCommand ("A00000039 FETCH 1:* (UID FLAGS INTERNALDATE RFC822.SIZE ENVELOPE BODYSTRUCTURE MODSEQ BODY.PEEK[HEADER.FIELDS (REFERENCES)])\r\n", "dovecot.fetch4.txt")); + commands.Add (new ImapReplayCommand ("A00000040 FETCH 1:14 (UID FLAGS INTERNALDATE RFC822.SIZE ENVELOPE BODYSTRUCTURE MODSEQ BODY.PEEK[HEADER.FIELDS (REFERENCES)])\r\n", "dovecot.fetch4.txt")); + commands.Add (new ImapReplayCommand ("A00000041 UID FETCH 1 (BODY.PEEK[])\r\n", "dovecot.getbodypart.txt")); + commands.Add (new ImapReplayCommand ("A00000042 FETCH 1 (BODY.PEEK[])\r\n", "dovecot.getbodypart.txt")); + commands.Add (new ImapReplayCommand ("A00000043 UID FETCH 2 (BODY.PEEK[1.MIME] BODY.PEEK[1])\r\n", "dovecot.getbodypart1.txt")); + commands.Add (new ImapReplayCommand ("A00000044 FETCH 2 (BODY.PEEK[1.MIME] BODY.PEEK[1])\r\n", "dovecot.getbodypart1.txt")); + commands.Add (new ImapReplayCommand ("A00000045 UID FETCH 1 (BODY.PEEK[HEADER])\r\n", "dovecot.getmessageheaders.txt")); + commands.Add (new ImapReplayCommand ("A00000046 FETCH 1 (BODY.PEEK[HEADER])\r\n", "dovecot.getmessageheaders.txt")); + commands.Add (new ImapReplayCommand ("A00000047 UID FETCH 1 (BODY.PEEK[HEADER])\r\n", "dovecot.getmessageheaders.txt")); + commands.Add (new ImapReplayCommand ("A00000048 FETCH 1 (BODY.PEEK[HEADER])\r\n", "dovecot.getmessageheaders.txt")); + commands.Add (new ImapReplayCommand ("A00000049 UID FETCH 2 (BODY.PEEK[1.MIME])\r\n", "dovecot.getbodypartheaders.txt")); + commands.Add (new ImapReplayCommand ("A00000050 FETCH 2 (BODY.PEEK[1.MIME])\r\n", "dovecot.getbodypartheaders.txt")); + commands.Add (new ImapReplayCommand ("A00000051 UID FETCH 1 (BODY.PEEK[]<128.64>)\r\n", "dovecot.getstream.txt")); + commands.Add (new ImapReplayCommand ("A00000052 UID FETCH 1 (BODY.PEEK[]<128.64>)\r\n", "dovecot.getstream2.txt")); + commands.Add (new ImapReplayCommand ("A00000053 FETCH 1 (BODY.PEEK[]<128.64>)\r\n", "dovecot.getstream.txt")); + commands.Add (new ImapReplayCommand ("A00000054 FETCH 1 (BODY.PEEK[]<128.64>)\r\n", "dovecot.getstream2.txt")); + commands.Add (new ImapReplayCommand ("A00000055 UID FETCH 1 (BODY.PEEK[HEADER.FIELDS (MIME-VERSION CONTENT-TYPE)])\r\n", "dovecot.getstream-section.txt")); + commands.Add (new ImapReplayCommand ("A00000056 FETCH 1 (BODY.PEEK[HEADER.FIELDS (MIME-VERSION CONTENT-TYPE)])\r\n", "dovecot.getstream-section2.txt")); + commands.Add (new ImapReplayCommand ("A00000057 UID STORE 1:14 (UNCHANGEDSINCE 3) +FLAGS.SILENT (\\Deleted $MailKit)\r\n", "dovecot.store-deleted-custom.txt")); + commands.Add (new ImapReplayCommand ("A00000058 STORE 1:7 (UNCHANGEDSINCE 5) FLAGS.SILENT (\\Deleted \\Seen $MailKit)\r\n", "dovecot.setflags-unchangedsince.txt")); + commands.Add (new ImapReplayCommand ("A00000059 UID SEARCH RETURN (ALL) UID 1:14 OR NEW OR OLD OR ANSWERED OR DELETED OR DRAFT OR FLAGGED OR RECENT OR UNANSWERED OR UNDELETED OR UNDRAFT OR UNFLAGGED OR UNSEEN OR KEYWORD $MailKit UNKEYWORD $MailKit\r\n", "dovecot.search-uids.txt")); + commands.Add (new ImapReplayCommand ("A00000060 UID SEARCH RETURN (ALL RELEVANCY COUNT MIN MAX) UID 1:14 LARGER 256 SMALLER 512\r\n", "dovecot.search-uids-options.txt")); + commands.Add (new ImapReplayCommand ("A00000061 UID SORT RETURN (ALL) (REVERSE DATE SUBJECT DISPLAYFROM SIZE) US-ASCII OR OR (SENTBEFORE 12-Oct-2016 SENTSINCE 10-Oct-2016) NOT SENTON 11-Oct-2016 OR (BEFORE 12-Oct-2016 SINCE 10-Oct-2016) NOT ON 11-Oct-2016\r\n", "dovecot.sort-by-date.txt")); + commands.Add (new ImapReplayCommand ("A00000062 UID SORT RETURN (ALL) (FROM TO CC) US-ASCII UID 1:14 OR BCC xyz OR CC xyz OR FROM xyz OR TO xyz OR SUBJECT xyz OR HEADER Message-Id mimekit.net OR BODY \"This is the message body.\" TEXT message\r\n", "dovecot.sort-by-strings.txt")); + commands.Add (new ImapReplayCommand ("A00000063 UID SORT RETURN (ALL RELEVANCY COUNT MIN MAX) (DISPLAYTO) US-ASCII UID 1:14 OLDER 1 YOUNGER 3600\r\n", "dovecot.sort-uids-options.txt")); + commands.Add (new ImapReplayCommand ("A00000064 UID SEARCH ALL\r\n", "dovecot.search-raw.txt")); + commands.Add (new ImapReplayCommand ("A00000065 UID SORT (REVERSE ARRIVAL) US-ASCII ALL\r\n", "dovecot.sort-raw.txt")); + commands.Add (new ImapReplayCommand ("A00000066 UID FETCH 1:* (BODY.PEEK[])\r\n", "dovecot.getstreams1.txt")); + commands.Add (new ImapReplayCommand ("A00000067 FETCH 1:3 (UID BODY.PEEK[])\r\n", "dovecot.getstreams1.txt")); + commands.Add (new ImapReplayCommand ("A00000068 FETCH 1:* (UID BODY.PEEK[])\r\n", "dovecot.getstreams2.txt")); + commands.Add (new ImapReplayCommand ("A00000069 EXPUNGE\r\n", "dovecot.expunge.txt")); + commands.Add (new ImapReplayCommand ("A00000070 CLOSE\r\n", ImapReplayCommandResponse.OK)); + commands.Add (new ImapReplayCommand ("A00000071 NOOP\r\n", "dovecot.noop+alert.txt")); + commands.Add (new ImapReplayCommand ("A00000072 LOGOUT\r\n", "gmail.logout.txt")); + + return commands; + } + + [Test] + public void TestDovecot () + { + var expectedFlags = MessageFlags.Answered | MessageFlags.Flagged | MessageFlags.Deleted | MessageFlags.Seen | MessageFlags.Draft; + var expectedPermanentFlags = expectedFlags | MessageFlags.UserDefined; + var commands = CreateDovecotCommands (out var internalDates, out var messages, out var flags); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + + Assert.That (client.Capabilities, Is.EqualTo (DovecotInitialCapabilities)); + Assert.That (client.AuthenticationMechanisms, Has.Count.EqualTo (4)); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN"), "Expected SASL PLAIN auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("DIGEST-MD5"), "Expected SASL DIGEST-MD5 auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("CRAM-MD5"), "Expected SASL CRAM-MD5 auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("NTLM"), "Expected SASL NTLM auth mechanism"); + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities, Is.EqualTo (DovecotAuthenticatedCapabilities)); + Assert.That (client.InternationalizationLevel, Is.EqualTo (1), "Expected I18NLEVEL=1"); + Assert.That (client.ThreadingAlgorithms, Does.Contain (ThreadingAlgorithm.OrderedSubject), "Expected THREAD=ORDEREDSUBJECT"); + Assert.That (client.ThreadingAlgorithms, Does.Contain (ThreadingAlgorithm.References), "Expected THREAD=REFERENCES"); + // TODO: verify CONTEXT=SEARCH + + var personal = client.GetFolder (client.PersonalNamespaces[0]); + + Assert.That (client.Inbox.Supports (FolderFeature.AccessRights), Is.False); + Assert.That (client.Inbox.Supports (FolderFeature.Annotations), Is.False); + Assert.That (client.Inbox.Supports (FolderFeature.Metadata), Is.False); + Assert.That (client.Inbox.Supports (FolderFeature.ModSequences), Is.False); // not supported until opened + Assert.That (client.Inbox.Supports (FolderFeature.QuickResync), Is.False); // not supported until it is enabled + Assert.That (client.Inbox.Supports (FolderFeature.Quotas), Is.False); + Assert.That (client.Inbox.Supports (FolderFeature.Sorting), Is.True); + Assert.That (client.Inbox.Supports (FolderFeature.Threading), Is.True); + Assert.That (client.Inbox.Supports (FolderFeature.UTF8), Is.False); + + // Make sure these all throw NotSupportedException Assert.Throws (() => client.EnableUTF8 ()); Assert.Throws (() => client.Inbox.GetAccessRights ("smith")); Assert.Throws (() => client.Inbox.GetMyAccessRights ()); var rights = new AccessRights ("lrswida"); - Assert.Throws (() => client.Inbox.AddAccessRights ("smith", rights)); - Assert.Throws (() => client.Inbox.RemoveAccessRights ("smith", rights)); - Assert.Throws (() => client.Inbox.SetAccessRights ("smith", rights)); - Assert.Throws (() => client.Inbox.RemoveAccess ("smith")); - Assert.Throws (() => client.Inbox.GetQuota ()); - Assert.Throws (() => client.Inbox.SetQuota (null, null)); - Assert.Throws (() => client.GetMetadata (MetadataTag.PrivateComment)); - Assert.Throws (() => client.GetMetadata (new MetadataTag[] { MetadataTag.PrivateComment })); - Assert.Throws (() => client.SetMetadata (new MetadataCollection ())); + Assert.Throws (() => client.Inbox.AddAccessRights ("smith", rights)); + Assert.Throws (() => client.Inbox.RemoveAccessRights ("smith", rights)); + Assert.Throws (() => client.Inbox.SetAccessRights ("smith", rights)); + Assert.Throws (() => client.Inbox.RemoveAccess ("smith")); + Assert.Throws (() => client.Inbox.GetQuota ()); + Assert.Throws (() => client.Inbox.SetQuota (null, null)); + Assert.Throws (() => client.GetMetadata (MetadataTag.PrivateComment)); + Assert.Throws (() => client.GetMetadata (new MetadataTag[] { MetadataTag.PrivateComment })); + Assert.Throws (() => client.SetMetadata (new MetadataCollection ())); + var labels = new string[] { "Label1", "Label2" }; + Assert.Throws (() => client.Inbox.AddLabels (UniqueId.MinValue, labels, true)); + Assert.Throws (() => client.Inbox.AddLabels (UniqueIdRange.All, labels, true)); + Assert.Throws (() => client.Inbox.AddLabels (UniqueIdRange.All, 1, labels, true)); + Assert.Throws (() => client.Inbox.AddLabels (0, labels, true)); + Assert.Throws (() => client.Inbox.AddLabels (new int[] { 0 }, labels, true)); + Assert.Throws (() => client.Inbox.AddLabels (new int[] { 0 }, 1, labels, true)); + Assert.Throws (() => client.Inbox.RemoveLabels (UniqueId.MinValue, labels, true)); + Assert.Throws (() => client.Inbox.RemoveLabels (UniqueIdRange.All, labels, true)); + Assert.Throws (() => client.Inbox.RemoveLabels (UniqueIdRange.All, 1, labels, true)); + Assert.Throws (() => client.Inbox.RemoveLabels (0, labels, true)); + Assert.Throws (() => client.Inbox.RemoveLabels (new int[] { 0 }, labels, true)); + Assert.Throws (() => client.Inbox.RemoveLabels (new int[] { 0 }, 1, labels, true)); + Assert.Throws (() => client.Inbox.SetLabels (UniqueId.MinValue, labels, true)); + Assert.Throws (() => client.Inbox.SetLabels (UniqueIdRange.All, labels, true)); + Assert.Throws (() => client.Inbox.SetLabels (UniqueIdRange.All, 1, labels, true)); + Assert.Throws (() => client.Inbox.SetLabels (0, labels, true)); + Assert.Throws (() => client.Inbox.SetLabels (new int[] { 0 }, labels, true)); + Assert.Throws (() => client.Inbox.SetLabels (new int[] { 0 }, 1, labels, true)); + + try { + client.EnableQuickResync (); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception when enabling QRESYNC: {ex}"); + } + + Assert.That (client.Inbox.Supports (FolderFeature.QuickResync), Is.True); + + // take advantage of LIST-STATUS to get top-level personal folders... + var statusItems = StatusItems.Count | StatusItems.HighestModSeq | StatusItems.Recent | StatusItems.UidNext | StatusItems.UidValidity | StatusItems.Unread; + + var folders = personal.GetSubfolders (statusItems, false).ToArray (); + Assert.That (folders, Has.Length.EqualTo (7), "Expected 7 folders"); + + var expectedFolderNames = new [] { "Archives", "Drafts", "Junk", "Sent Messages", "Trash", "INBOX", "NIL" }; + var expectedUidValidities = new [] { 1436832059, 1436832060, 1436832061, 1436832062, 1436832063, 1436832057, 1436832057 }; + var expectedHighestModSeq = new [] { 1, 1, 1, 1, 1, 15, 1 }; + var expectedMessages = new [] { 0, 0, 0, 0, 0, 4, 0 }; + var expectedUidNext = new [] { 1, 1, 1, 1, 1, 5, 1 }; + var expectedRecent = new [] { 0, 0, 0, 0, 0, 0, 0 }; + var expectedUnseen = new [] { 0, 0, 0, 0, 0, 0, 0 }; + + for (int i = 0; i < folders.Length; i++) { + Assert.That (folders[i].FullName, Is.EqualTo (expectedFolderNames[i]), "FullName did not match"); + Assert.That (folders[i].Name, Is.EqualTo (expectedFolderNames[i]), "Name did not match"); + Assert.That (folders[i].UidValidity, Is.EqualTo (expectedUidValidities[i]), "UidValidity did not match"); + Assert.That (folders[i].HighestModSeq, Is.EqualTo (expectedHighestModSeq[i]), "HighestModSeq did not match"); + Assert.That (folders[i], Has.Count.EqualTo (expectedMessages[i]), "Count did not match"); + Assert.That (folders[i].Recent, Is.EqualTo (expectedRecent[i]), "Recent did not match"); + Assert.That (folders[i].Unread, Is.EqualTo (expectedUnseen[i]), "Unread did not match"); + } + + var unitTests = personal.Create ("UnitTests", false); + Assert.That (unitTests.Attributes, Is.EqualTo (FolderAttributes.HasNoChildren), "Unexpected UnitTests folder attributes"); + + var folder = unitTests.Create ("Messages", true); + Assert.That (folder.Attributes, Is.EqualTo (FolderAttributes.HasNoChildren), "Unexpected UnitTests.Messages folder attributes"); + //Assert.That (unitTests.Attributes, Is.EqualTo (FolderAttributes.HasChildren), "Expected UnitTests Attributes to be updated"); + + // Use MULTIAPPEND to append some test messages + var appended = folder.Append (messages, flags, internalDates); + Assert.That (appended, Has.Count.EqualTo (8), "Unexpected number of messages appended"); + foreach (var message in messages) + message.Dispose (); + + // SELECT the folder so that we can test some stuff + var access = folder.Open (FolderAccess.ReadWrite); + Assert.That (folder.Supports (FolderFeature.ModSequences), Is.True); + Assert.That (folder.PermanentFlags, Is.EqualTo (expectedPermanentFlags), "UnitTests.Messages PERMANENTFLAGS"); + Assert.That (folder.AcceptedFlags, Is.EqualTo (expectedFlags), "UnitTests.Messages FLAGS"); + Assert.That (folder, Has.Count.EqualTo (8), "UnitTests.Messages EXISTS"); + Assert.That (folder.Recent, Is.EqualTo (8), "UnitTests.Messages RECENT"); + Assert.That (folder.FirstUnread, Is.EqualTo (0), "UnitTests.Messages UNSEEN"); + Assert.That (folder.UidValidity, Is.EqualTo (1436832084U), "UnitTests.Messages UIDVALIDITY"); + Assert.That (folder.UidNext.Value.Id, Is.EqualTo (9), "UnitTests.Messages UIDNEXT"); + Assert.That (folder.HighestModSeq, Is.EqualTo (2UL), "UnitTests.Messages HIGHESTMODSEQ"); + Assert.That (access, Is.EqualTo (FolderAccess.ReadWrite), "Expected UnitTests.Messages to be opened in READ-WRITE mode"); + + // Keep track of various folder events + var flagsChanged = new List (); + var modSeqChanged = new List (); + var vanished = new List (); + bool recentChanged = false; + + folder.MessageFlagsChanged += (sender, e) => { + flagsChanged.Add (e); + }; + + folder.ModSeqChanged += (sender, e) => { + modSeqChanged.Add (e); + }; + + folder.MessagesVanished += (sender, e) => { + vanished.Add (e); + }; + + folder.RecentChanged += (sender, e) => { + recentChanged = true; + }; + + // Keep track of UIDVALIDITY and HIGHESTMODSEQ values for our QRESYNC test later + var highestModSeq = folder.HighestModSeq; + var uidValidity = folder.UidValidity; + + // Make some FLAGS changes to our messages so we can test QRESYNC + folder.AddFlags (appended, MessageFlags.Seen, true); + Assert.That (flagsChanged, Is.Empty, "Unexpected number of FlagsChanged events"); + Assert.That (modSeqChanged, Has.Count.EqualTo (8), "Unexpected number of ModSeqChanged events"); + for (int i = 0; i < modSeqChanged.Count; i++) { + Assert.That (modSeqChanged[i].Index, Is.EqualTo (i), $"Unexpected modSeqChanged[{i}].Index"); + Assert.That (modSeqChanged[i].UniqueId.Value.Id, Is.EqualTo (i + 1), $"Unexpected modSeqChanged[{i}].UniqueId"); + Assert.That (modSeqChanged[i].ModSeq, Is.EqualTo (3), $"Unexpected modSeqChanged[{i}].ModSeq"); + } + Assert.That (recentChanged, Is.False, "Unexpected RecentChanged event"); + modSeqChanged.Clear (); + flagsChanged.Clear (); + + var answered = new UniqueIdSet (SortOrder.Ascending) { + appended[0], // A + appended[1], // B + appended[2] // C + }; + folder.AddFlags (answered, MessageFlags.Answered, true); + Assert.That (flagsChanged, Is.Empty, "Unexpected number of FlagsChanged events"); + Assert.That (modSeqChanged, Has.Count.EqualTo (3), "Unexpected number of ModSeqChanged events"); + for (int i = 0; i < modSeqChanged.Count; i++) { + Assert.That (modSeqChanged[i].Index, Is.EqualTo (i), $"Unexpected modSeqChanged[{i}].Index"); + Assert.That (modSeqChanged[i].UniqueId.Value.Id, Is.EqualTo (i + 1), $"Unexpected modSeqChanged[{i}].UniqueId"); + Assert.That (modSeqChanged[i].ModSeq, Is.EqualTo (4), $"Unexpected modSeqChanged[{i}].ModSeq"); + } + Assert.That (recentChanged, Is.False, "Unexpected RecentChanged event"); + modSeqChanged.Clear (); + flagsChanged.Clear (); + + // Delete some messages so we can test that QRESYNC emits some MessageVanished events + // both now *and* when we use QRESYNC to re-open the folder + var deleted = new UniqueIdSet (SortOrder.Ascending) { + appended[7] // H + }; + folder.AddFlags (deleted, MessageFlags.Deleted, true); + Assert.That (flagsChanged, Is.Empty, "Unexpected number of FlagsChanged events"); + Assert.That (modSeqChanged, Has.Count.EqualTo (1), "Unexpected number of ModSeqChanged events"); + Assert.That (modSeqChanged[0].Index, Is.EqualTo (7), $"Unexpected modSeqChanged[{0}].Index"); + Assert.That (modSeqChanged[0].UniqueId.Value.Id, Is.EqualTo (8), $"Unexpected modSeqChanged[{0}].UniqueId"); + Assert.That (modSeqChanged[0].ModSeq, Is.EqualTo (5), $"Unexpected modSeqChanged[{0}].ModSeq"); + Assert.That (recentChanged, Is.False, "Unexpected RecentChanged event"); + modSeqChanged.Clear (); + flagsChanged.Clear (); + + folder.Expunge (deleted); + Assert.That (vanished, Has.Count.EqualTo (1), "Expected MessagesVanished event"); + Assert.That (vanished[0].UniqueIds, Has.Count.EqualTo (1), "Unexpected number of messages vanished"); + Assert.That (vanished[0].UniqueIds[0].Id, Is.EqualTo (8), "Unexpected UID for vanished message"); + Assert.That (vanished[0].Earlier, Is.False, "Expected EARLIER to be false"); + Assert.That (recentChanged, Is.True, "Expected RecentChanged event"); + recentChanged = false; + vanished.Clear (); + + Assert.That (folder.Supports (FolderFeature.Threading), Is.True, "Supports threading"); + Assert.That (folder.ThreadingAlgorithms, Does.Contain (ThreadingAlgorithm.References), "Supports threading by References"); + Assert.That (folder.ThreadingAlgorithms, Does.Contain (ThreadingAlgorithm.OrderedSubject), "Supports threading by OrderedSubject"); + + // Verify that THREAD works correctly + var threaded = folder.Thread (ThreadingAlgorithm.References, SearchQuery.All); + Assert.That (threaded, Has.Count.EqualTo (2), "Unexpected number of root nodes in threaded results"); + + threaded = folder.Thread (UniqueIdRange.All, ThreadingAlgorithm.OrderedSubject, SearchQuery.All); + Assert.That (threaded, Has.Count.EqualTo (7), "Unexpected number of root nodes in threaded results"); + + // UNSELECT the folder so we can re-open it using QRESYNC + folder.Close (); + + // Use QRESYNC to get the changes since last time we opened the folder + Assert.That (folder.Supports (FolderFeature.QuickResync), Is.True, "Supports QRESYNC"); + access = folder.Open (FolderAccess.ReadWrite, uidValidity, highestModSeq, appended); + Assert.That (access, Is.EqualTo (FolderAccess.ReadWrite), "Expected UnitTests.Messages to be opened in READ-WRITE mode"); + Assert.That (flagsChanged, Has.Count.EqualTo (7), "Unexpected number of MessageFlagsChanged events"); + Assert.That (modSeqChanged, Has.Count.EqualTo (7), "Unexpected number of ModSeqChanged events"); + for (int i = 0; i < flagsChanged.Count; i++) { + var messageFlags = MessageFlags.Seen | MessageFlags.Draft; + + if (i < 3) + messageFlags |= MessageFlags.Answered; + + Assert.That (flagsChanged[i].Index, Is.EqualTo (i), $"Unexpected value for flagsChanged[{i}].Index"); + Assert.That (flagsChanged[i].UniqueId.Value.Id, Is.EqualTo ((uint) (i + 1)), $"Unexpected value for flagsChanged[{i}].UniqueId"); + Assert.That (flagsChanged[i].Flags, Is.EqualTo (messageFlags), $"Unexpected value for flagsChanged[{i}].Flags"); + + Assert.That (modSeqChanged[i].Index, Is.EqualTo (i), $"Unexpected value for modSeqChanged[{i}].Index"); + if (i < 3) + Assert.That (modSeqChanged[i].ModSeq, Is.EqualTo (4), $"Unexpected value for modSeqChanged[{i}].ModSeq"); + else + Assert.That (modSeqChanged[i].ModSeq, Is.EqualTo (3), $"Unexpected value for modSeqChanged[{i}].ModSeq"); + } + modSeqChanged.Clear (); + flagsChanged.Clear (); + + Assert.That (vanished, Has.Count.EqualTo (1), "Unexpected number of MessagesVanished events"); + Assert.That (vanished[0].Earlier, Is.True, "Expected VANISHED EARLIER"); + Assert.That (vanished[0].UniqueIds, Has.Count.EqualTo (1), "Unexpected number of messages vanished"); + Assert.That (vanished[0].UniqueIds[0].Id, Is.EqualTo (8), "Unexpected UID for vanished message"); + vanished.Clear (); + + Assert.Throws (() => folder.Search (SearchQuery.GMailMessageId (1))); + Assert.Throws (() => folder.Search (SearchQuery.GMailThreadId (1))); + Assert.Throws (() => folder.Search (SearchQuery.HasGMailLabel ("Custom Label"))); + Assert.Throws (() => folder.Search (SearchQuery.GMailRawSearch ("has:attachment in:unread"))); + Assert.Throws (() => folder.Search (SearchQuery.Fuzzy (SearchQuery.SubjectContains ("some fuzzy text")))); + Assert.Throws (() => folder.Search (SearchQuery.Filter (new MetadataTag ("/private/filters/values/saved-search")))); + Assert.Throws (() => folder.Search (SearchQuery.Filter ("saved-search"))); + Assert.Throws (() => folder.Search (SearchQuery.SaveDateSupported)); + Assert.Throws (() => folder.Search (SearchQuery.SavedBefore (DateTime.Now))); + Assert.Throws (() => folder.Search (SearchQuery.SavedOn (DateTime.Now))); + Assert.Throws (() => folder.Search (SearchQuery.SavedSince (DateTime.Now))); + + // Use SEARCH and FETCH to get the same info + var searchOptions = SearchOptions.All | SearchOptions.Count | SearchOptions.Min | SearchOptions.Max | SearchOptions.Relevancy; + var changed = folder.Search (searchOptions, SearchQuery.ChangedSince (highestModSeq)); + Assert.That (changed.UniqueIds, Has.Count.EqualTo (7), "Unexpected number of UIDs"); + Assert.That (changed.Relevancy, Has.Count.EqualTo (changed.Count), "Unexpected number of relevancy scores"); + Assert.That (changed.ModSeq.HasValue, Is.True, "Expected the ModSeq property to be set"); + Assert.That (changed.ModSeq.Value, Is.EqualTo (4), "Unexpected ModSeq value"); + Assert.That (changed.Min.Value.Id, Is.EqualTo (1), "Unexpected Min"); + Assert.That (changed.Max.Value.Id, Is.EqualTo (7), "Unexpected Max"); + Assert.That (changed.Count, Is.EqualTo (7), "Unexpected Count"); + + var fetched = folder.Fetch (changed.UniqueIds, MessageSummaryItems.UniqueId | MessageSummaryItems.Flags | MessageSummaryItems.ModSeq); + Assert.That (fetched, Has.Count.EqualTo (7), "Unexpected number of messages fetched"); + for (int i = 0; i < fetched.Count; i++) { + Assert.That (fetched[i].Index, Is.EqualTo (i), "Unexpected Index"); + Assert.That (fetched[i].UniqueId.Id, Is.EqualTo (i + 1), "Unexpected UniqueId"); + } + + // or... we could just use a single UID FETCH command like so: + fetched = folder.Fetch (UniqueIdRange.All, highestModSeq, MessageSummaryItems.UniqueId | MessageSummaryItems.Flags | MessageSummaryItems.ModSeq); + for (int i = 0; i < fetched.Count; i++) { + Assert.That (fetched[i].Index, Is.EqualTo (i), "Unexpected Index"); + Assert.That (fetched[i].UniqueId.Id, Is.EqualTo (i + 1), "Unexpected UniqueId"); + } + Assert.That (fetched, Has.Count.EqualTo (7), "Unexpected number of messages fetched"); + Assert.That (vanished, Has.Count.EqualTo (1), "Unexpected number of MessagesVanished events"); + Assert.That (vanished[0].Earlier, Is.True, "Expected VANISHED EARLIER"); + Assert.That (vanished[0].UniqueIds, Has.Count.EqualTo (1), "Unexpected number of messages vanished"); + Assert.That (vanished[0].UniqueIds[0].Id, Is.EqualTo (8), "Unexpected UID for vanished message"); + vanished.Clear (); + + // Use SORT to order by reverse arrival order + var orderBy = new OrderBy[] { new OrderBy (OrderByType.Arrival, SortOrder.Descending) }; + var sorted = folder.Sort (searchOptions, SearchQuery.All, orderBy); + Assert.That (sorted.UniqueIds, Has.Count.EqualTo (7), "Unexpected number of UIDs"); + Assert.That (sorted.Relevancy, Has.Count.EqualTo (sorted.Count), "Unexpected number of relevancy scores"); + for (int i = 0; i < sorted.UniqueIds.Count; i++) + Assert.That (sorted.UniqueIds[i].Id, Is.EqualTo (7 - i), $"Unexpected value for UniqueId[{i}]"); + Assert.That (sorted.ModSeq.HasValue, Is.False, "Expected the ModSeq property to be null"); + Assert.That (sorted.Min.Value.Id, Is.EqualTo (7), "Unexpected Min"); + Assert.That (sorted.Max.Value.Id, Is.EqualTo (1), "Unexpected Max"); + Assert.That (sorted.Count, Is.EqualTo (7), "Unexpected Count"); + + // Verify that optimizing NOT queries works correctly + var uids = folder.Search (SearchQuery.Not (SearchQuery.Deleted).And (SearchQuery.Not (SearchQuery.NotSeen))); + Assert.That (uids, Has.Count.EqualTo (7), "Unexpected number of UIDs"); + for (int i = 0; i < uids.Count; i++) + Assert.That (uids[i].Id, Is.EqualTo (i + 1), $"Unexpected value for uids[{i}]"); + + // Create a Destination folder to use for copying/moving messages to + var destination = (ImapFolder) unitTests.Create ("Destination", true); + Assert.That (destination.Attributes, Is.EqualTo (FolderAttributes.HasNoChildren), "Unexpected UnitTests.Destination folder attributes"); + + // COPY messages to the Destination folder + var copied = folder.CopyTo (uids, destination); + Assert.That (copied.Source, Has.Count.EqualTo (uids.Count), "Unexpected Source.Count"); + Assert.That (copied.Destination, Has.Count.EqualTo (uids.Count), "Unexpected Destination.Count"); + + // MOVE messages to the Destination folder + var moved = folder.MoveTo (uids, destination); + Assert.That (copied.Source, Has.Count.EqualTo (uids.Count), "Unexpected Source.Count"); + Assert.That (copied.Destination, Has.Count.EqualTo (uids.Count), "Unexpected Destination.Count"); + Assert.That (vanished, Has.Count.EqualTo (1), "Expected VANISHED event"); + vanished.Clear (); + + destination.Status (statusItems); + Assert.That (destination.UidValidity, Is.EqualTo (moved.Destination[0].Validity), "Unexpected UIDVALIDITY"); + + destination.MessageFlagsChanged += (sender, e) => { + flagsChanged.Add (e); + }; + + destination.ModSeqChanged += (sender, e) => { + modSeqChanged.Add (e); + }; + + destination.MessagesVanished += (sender, e) => { + vanished.Add (e); + }; + + destination.RecentChanged += (sender, e) => { + recentChanged = true; + }; + + destination.Open (FolderAccess.ReadWrite); + Assert.That (access, Is.EqualTo (FolderAccess.ReadWrite), "Expected UnitTests.Destination to be opened in READ-WRITE mode"); + + var fetchHeaders = new HashSet { + HeaderId.References, + HeaderId.XMailer + }; + + var indexes = new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13 }; + + // Fetch + modseq + fetched = destination.Fetch (UniqueIdRange.All, 1, MessageSummaryItems.Full | MessageSummaryItems.UniqueId | + MessageSummaryItems.BodyStructure | MessageSummaryItems.ModSeq | + MessageSummaryItems.References, fetchHeaders); + Assert.That (fetched, Has.Count.EqualTo (14), "Unexpected number of messages fetched"); + + fetched = destination.Fetch (0, -1, 1, MessageSummaryItems.Full | MessageSummaryItems.UniqueId | + MessageSummaryItems.BodyStructure | MessageSummaryItems.ModSeq | + MessageSummaryItems.References, fetchHeaders); + Assert.That (fetched, Has.Count.EqualTo (14), "Unexpected number of messages fetched"); + + fetched = destination.Fetch (indexes, 1, MessageSummaryItems.Full | MessageSummaryItems.UniqueId | + MessageSummaryItems.BodyStructure | MessageSummaryItems.ModSeq | + MessageSummaryItems.References, fetchHeaders); + Assert.That (fetched, Has.Count.EqualTo (14), "Unexpected number of messages fetched"); + + fetched = destination.Fetch (0, -1, 1, MessageSummaryItems.Full | MessageSummaryItems.UniqueId | + MessageSummaryItems.BodyStructure | MessageSummaryItems.ModSeq | + MessageSummaryItems.References); + Assert.That (fetched, Has.Count.EqualTo (14), "Unexpected number of messages fetched"); + + fetched = destination.Fetch (indexes, 1, MessageSummaryItems.Full | MessageSummaryItems.UniqueId | + MessageSummaryItems.BodyStructure | MessageSummaryItems.ModSeq | + MessageSummaryItems.References); + Assert.That (fetched, Has.Count.EqualTo (14), "Unexpected number of messages fetched"); + + // Fetch + fetched = destination.Fetch (UniqueIdRange.All, MessageSummaryItems.Full | MessageSummaryItems.UniqueId | + MessageSummaryItems.BodyStructure | MessageSummaryItems.ModSeq | + MessageSummaryItems.References, fetchHeaders); + Assert.That (fetched, Has.Count.EqualTo (14), "Unexpected number of messages fetched"); + + fetched = destination.Fetch (0, -1, MessageSummaryItems.Full | MessageSummaryItems.UniqueId | + MessageSummaryItems.BodyStructure | MessageSummaryItems.ModSeq | + MessageSummaryItems.References, fetchHeaders); + Assert.That (fetched, Has.Count.EqualTo (14), "Unexpected number of messages fetched"); + + fetched = destination.Fetch (indexes, MessageSummaryItems.Full | MessageSummaryItems.UniqueId | + MessageSummaryItems.BodyStructure | MessageSummaryItems.ModSeq | + MessageSummaryItems.References, fetchHeaders); + Assert.That (fetched, Has.Count.EqualTo (14), "Unexpected number of messages fetched"); + + fetched = destination.Fetch (0, -1, MessageSummaryItems.Full | MessageSummaryItems.UniqueId | + MessageSummaryItems.BodyStructure | MessageSummaryItems.ModSeq | + MessageSummaryItems.References); + Assert.That (fetched, Has.Count.EqualTo (14), "Unexpected number of messages fetched"); + + fetched = destination.Fetch (indexes, MessageSummaryItems.Full | MessageSummaryItems.UniqueId | + MessageSummaryItems.BodyStructure | MessageSummaryItems.ModSeq | + MessageSummaryItems.References); + Assert.That (fetched, Has.Count.EqualTo (14), "Unexpected number of messages fetched"); + + uids = new UniqueIdSet (SortOrder.Ascending); + + for (int i = 0; i < fetched.Count; i++) { + Assert.That (fetched[i].Index, Is.EqualTo (i), "Unexpected Index"); + Assert.That (fetched[i].UniqueId.Id, Is.EqualTo (i + 1), "Unexpected UniqueId"); + + uids.Add (fetched[i].UniqueId); + } + + using (var entity = destination.GetBodyPart (fetched[0].UniqueId, fetched[0].TextBody)) + Assert.That (entity, Is.InstanceOf ()); + + using (var entity = destination.GetBodyPart (fetched[0].Index, fetched[0].TextBody)) + Assert.That (entity, Is.InstanceOf ()); + + using (var entity = destination.GetBodyPart (fetched[1].UniqueId, fetched[1].TextBody)) + Assert.That (entity, Is.InstanceOf ()); + + using (var entity = destination.GetBodyPart (fetched[1].Index, fetched[1].TextBody)) + Assert.That (entity, Is.InstanceOf ()); + + var headers = destination.GetHeaders (fetched[0].UniqueId); + Assert.That (headers[HeaderId.From], Is.EqualTo ("Unit Tests "), "GetHeaders(UniqueId) failed to match From header"); + Assert.That (headers[HeaderId.Date], Is.EqualTo ("Sun, 02 Oct 2016 17:56:45 -0400"), "GetHeaders(UniqueId) failed to match Date header"); + Assert.That (headers[HeaderId.Subject], Is.EqualTo ("A"), "GetHeaders(UniqueId) failed to match Subject header"); + Assert.That (headers[HeaderId.MessageId], Is.EqualTo (""), "GetHeaders(UniqueId) failed to match Message-Id header"); + Assert.That (headers[HeaderId.To], Is.EqualTo ("Unit Tests "), "GetHeaders(UniqueId) failed to match To header"); + Assert.That (headers[HeaderId.MimeVersion], Is.EqualTo ("1.0"), "GetHeaders(UniqueId) failed to match MIME-Version header"); + Assert.That (headers[HeaderId.ContentType], Is.EqualTo ("text/plain; charset=utf-8"), "GetHeaders(UniqueId) failed to match Content-Type header"); + + headers = destination.GetHeaders (fetched[0].Index); + Assert.That (headers[HeaderId.From], Is.EqualTo ("Unit Tests "), "GetHeaders(int) failed to match From header"); + Assert.That (headers[HeaderId.Date], Is.EqualTo ("Sun, 02 Oct 2016 17:56:45 -0400"), "GetHeaders(UniqueId) failed to match Date header"); + Assert.That (headers[HeaderId.Subject], Is.EqualTo ("A"), "GetHeaders(UniqueId) failed to match Subject header"); + Assert.That (headers[HeaderId.MessageId], Is.EqualTo (""), "GetHeaders(UniqueId) failed to match Message-Id header"); + Assert.That (headers[HeaderId.To], Is.EqualTo ("Unit Tests "), "GetHeaders(UniqueId) failed to match To header"); + Assert.That (headers[HeaderId.MimeVersion], Is.EqualTo ("1.0"), "GetHeaders(UniqueId) failed to match MIME-Version header"); + Assert.That (headers[HeaderId.ContentType], Is.EqualTo ("text/plain; charset=utf-8"), "GetHeaders(UniqueId) failed to match Content-Type header"); + + headers = destination.GetHeaders (fetched[0].UniqueId, fetched[0].TextBody); + Assert.That (headers[HeaderId.From], Is.EqualTo ("Unit Tests "), "GetHeaders(UniqueId, BodyPart) failed to match From header"); + Assert.That (headers[HeaderId.Date], Is.EqualTo ("Sun, 02 Oct 2016 17:56:45 -0400"), "GetHeaders(UniqueId) failed to match Date header"); + Assert.That (headers[HeaderId.Subject], Is.EqualTo ("A"), "GetHeaders(UniqueId) failed to match Subject header"); + Assert.That (headers[HeaderId.MessageId], Is.EqualTo (""), "GetHeaders(UniqueId) failed to match Message-Id header"); + Assert.That (headers[HeaderId.To], Is.EqualTo ("Unit Tests "), "GetHeaders(UniqueId) failed to match To header"); + Assert.That (headers[HeaderId.MimeVersion], Is.EqualTo ("1.0"), "GetHeaders(UniqueId) failed to match MIME-Version header"); + Assert.That (headers[HeaderId.ContentType], Is.EqualTo ("text/plain; charset=utf-8"), "GetHeaders(UniqueId) failed to match Content-Type header"); + + headers = destination.GetHeaders (fetched[0].Index, fetched[0].TextBody); + Assert.That (headers[HeaderId.From], Is.EqualTo ("Unit Tests "), "GetHeaders(int, BodyPart) failed to match From header"); + Assert.That (headers[HeaderId.Date], Is.EqualTo ("Sun, 02 Oct 2016 17:56:45 -0400"), "GetHeaders(UniqueId) failed to match Date header"); + Assert.That (headers[HeaderId.Subject], Is.EqualTo ("A"), "GetHeaders(UniqueId) failed to match Subject header"); + Assert.That (headers[HeaderId.MessageId], Is.EqualTo (""), "GetHeaders(UniqueId) failed to match Message-Id header"); + Assert.That (headers[HeaderId.To], Is.EqualTo ("Unit Tests "), "GetHeaders(UniqueId) failed to match To header"); + Assert.That (headers[HeaderId.MimeVersion], Is.EqualTo ("1.0"), "GetHeaders(UniqueId) failed to match MIME-Version header"); + Assert.That (headers[HeaderId.ContentType], Is.EqualTo ("text/plain; charset=utf-8"), "GetHeaders(UniqueId) failed to match Content-Type header"); + + headers = destination.GetHeaders (fetched[1].UniqueId, fetched[1].TextBody); + Assert.That (headers[HeaderId.ContentType], Is.EqualTo ("text/plain; charset=utf-8"), "GetHeaders(UniqueId) failed to match Content-Type header"); + + headers = destination.GetHeaders (fetched[1].Index, fetched[1].TextBody); + Assert.That (headers[HeaderId.ContentType], Is.EqualTo ("text/plain; charset=utf-8"), "GetHeaders(UniqueId) failed to match Content-Type header"); + + using (var stream = destination.GetStream (fetched[0].UniqueId, 128, 64)) { + Assert.That (stream.Length, Is.EqualTo (64), "Unexpected stream length"); + + string text; + using (var reader = new StreamReader (stream)) + text = reader.ReadToEnd (); + + Assert.That (text, Is.EqualTo ("nit Tests \r\nMIME-Version: 1.0\r\nContent-T")); + } + + using (var stream = destination.GetStream (fetched[0].UniqueId, "", 128, 64)) { + Assert.That (stream.Length, Is.EqualTo (64), "Unexpected stream length"); + + string text; + using (var reader = new StreamReader (stream)) + text = reader.ReadToEnd (); + + Assert.That (text, Is.EqualTo ("nit Tests \r\nMIME-Version: 1.0\r\nContent-T")); + } + + using (var stream = destination.GetStream (fetched[0].Index, 128, 64)) { + Assert.That (stream.Length, Is.EqualTo (64), "Unexpected stream length"); + + string text; + using (var reader = new StreamReader (stream)) + text = reader.ReadToEnd (); + + Assert.That (text, Is.EqualTo ("nit Tests \r\nMIME-Version: 1.0\r\nContent-T")); + } + + using (var stream = destination.GetStream (fetched[0].Index, "", 128, 64)) { + Assert.That (stream.Length, Is.EqualTo (64), "Unexpected stream length"); + + string text; + using (var reader = new StreamReader (stream)) + text = reader.ReadToEnd (); + + Assert.That (text, Is.EqualTo ("nit Tests \r\nMIME-Version: 1.0\r\nContent-T")); + } + + using (var stream = destination.GetStream (fetched[0].UniqueId, "HEADER.FIELDS (MIME-VERSION CONTENT-TYPE)")) { + Assert.That (stream.Length, Is.EqualTo (62), "Unexpected stream length"); + + string text; + using (var reader = new StreamReader (stream)) + text = reader.ReadToEnd (); + + Assert.That (text, Is.EqualTo ("MIME-Version: 1.0\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n")); + } + + using (var stream = destination.GetStream (fetched[0].Index, "HEADER.FIELDS (MIME-VERSION CONTENT-TYPE)")) { + Assert.That (stream.Length, Is.EqualTo (62), "Unexpected stream length"); + + string text; + using (var reader = new StreamReader (stream)) + text = reader.ReadToEnd (); + + Assert.That (text, Is.EqualTo ("MIME-Version: 1.0\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n")); + } + + var custom = new HashSet { "$MailKit" }; + + var unchanged1 = destination.AddFlags (uids, destination.HighestModSeq, MessageFlags.Deleted, custom, true); + Assert.That (modSeqChanged, Has.Count.EqualTo (14), "Unexpected number of ModSeqChanged events"); + Assert.That (destination.HighestModSeq, Is.EqualTo (5)); + for (int i = 0; i < modSeqChanged.Count; i++) { + Assert.That (modSeqChanged[i].Index, Is.EqualTo (i), $"Unexpected value for modSeqChanged[{i}].Index"); + Assert.That (modSeqChanged[i].ModSeq, Is.EqualTo (5), $"Unexpected value for modSeqChanged[{i}].ModSeq"); + } + Assert.That (unchanged1, Has.Count.EqualTo (2), "[MODIFIED uid-set]"); + Assert.That (unchanged1[0].Id, Is.EqualTo (7), "unchanged uids[0]"); + Assert.That (unchanged1[1].Id, Is.EqualTo (9), "unchanged uids[1]"); + modSeqChanged.Clear (); + + var unchanged2 = destination.SetFlags (new int[] { 0, 1, 2, 3, 4, 5, 6 }, destination.HighestModSeq, MessageFlags.Seen | MessageFlags.Deleted, custom, true); + Assert.That (modSeqChanged, Has.Count.EqualTo (7), "Unexpected number of ModSeqChanged events"); + Assert.That (destination.HighestModSeq, Is.EqualTo (6)); + for (int i = 0; i < modSeqChanged.Count; i++) { + Assert.That (modSeqChanged[i].Index, Is.EqualTo (i), $"Unexpected value for modSeqChanged[{i}].Index"); + Assert.That (modSeqChanged[i].ModSeq, Is.EqualTo (6), $"Unexpected value for modSeqChanged[{i}].ModSeq"); + } + Assert.That (unchanged2, Has.Count.EqualTo (2), "[MODIFIED seq-set]"); + Assert.That (unchanged2[0], Is.EqualTo (6), "unchanged indexes[0]"); + Assert.That (unchanged2[1], Is.EqualTo (8), "unchanged indexes[1]"); + modSeqChanged.Clear (); + + var results = destination.Search (uids, SearchQuery.New.Or (SearchQuery.Old.Or (SearchQuery.Answered.Or (SearchQuery.Deleted.Or (SearchQuery.Draft.Or (SearchQuery.Flagged.Or (SearchQuery.Recent.Or (SearchQuery.NotAnswered.Or (SearchQuery.NotDeleted.Or (SearchQuery.NotDraft.Or (SearchQuery.NotFlagged.Or (SearchQuery.NotSeen.Or (SearchQuery.HasKeyword ("$MailKit").Or (SearchQuery.NotKeyword ("$MailKit"))))))))))))))); + Assert.That (results, Has.Count.EqualTo (14), "Unexpected number of UIDs"); + + var matches = destination.Search (searchOptions, uids, SearchQuery.LargerThan (256).And (SearchQuery.SmallerThan (512))); + var expectedMatchedUids = new uint[] { 2, 3, 4, 5, 6, 9, 10, 11, 12, 13 }; + Assert.That (matches.Count, Is.EqualTo (10), "Unexpected COUNT"); + Assert.That (matches.Max.Value.Id, Is.EqualTo (13), "Unexpected MAX"); + Assert.That (matches.Min.Value.Id, Is.EqualTo (2), "Unexpected MIN"); + Assert.That (matches.UniqueIds, Has.Count.EqualTo (10), "Unexpected number of UIDs"); + for (int i = 0; i < matches.UniqueIds.Count; i++) + Assert.That (matches.UniqueIds[i].Id, Is.EqualTo (expectedMatchedUids[i])); + Assert.That (matches.Relevancy, Has.Count.EqualTo (matches.Count), "Unexpected number of relevancy scores"); + + orderBy = new OrderBy[] { OrderBy.ReverseDate, OrderBy.Subject, OrderBy.DisplayFrom, OrderBy.Size }; + var sentDateQuery = SearchQuery.Or (SearchQuery.And (SearchQuery.SentBefore (new DateTime (2016, 10, 12)), SearchQuery.SentSince (new DateTime (2016, 10, 10))), SearchQuery.Not (SearchQuery.SentOn (new DateTime (2016, 10, 11)))); + var deliveredDateQuery = SearchQuery.Or (SearchQuery.And (SearchQuery.DeliveredBefore (new DateTime (2016, 10, 12)), SearchQuery.DeliveredAfter (new DateTime (2016, 10, 10))), SearchQuery.Not (SearchQuery.DeliveredOn (new DateTime (2016, 10, 11)))); + results = destination.Sort (sentDateQuery.Or (deliveredDateQuery), orderBy); + var expectedSortByDateResults = new uint[] { 7, 14, 6, 13, 5, 12, 4, 11, 3, 10, 2, 9, 1, 8 }; + Assert.That (results, Has.Count.EqualTo (14), "Unexpected number of UIDs"); + for (int i = 0; i < results.Count; i++) + Assert.That (results[i].Id, Is.EqualTo (expectedSortByDateResults[i])); + + var stringQuery = SearchQuery.BccContains ("xyz").Or (SearchQuery.CcContains ("xyz").Or (SearchQuery.FromContains ("xyz").Or (SearchQuery.ToContains ("xyz").Or (SearchQuery.SubjectContains ("xyz").Or (SearchQuery.HeaderContains ("Message-Id", "mimekit.net").Or (SearchQuery.BodyContains ("This is the message body.").Or (SearchQuery.MessageContains ("message")))))))); + orderBy = new OrderBy[] { OrderBy.From, OrderBy.To, OrderBy.Cc }; + results = destination.Sort (uids, stringQuery, orderBy); + Assert.That (results, Has.Count.EqualTo (14), "Unexpected number of UIDs"); + for (int i = 0; i < results.Count; i++) + Assert.That (results[i].Id, Is.EqualTo (i + 1)); + + orderBy = new OrderBy[] { OrderBy.DisplayTo }; + matches = destination.Sort (searchOptions, uids, SearchQuery.OlderThan (1).And (SearchQuery.YoungerThan (3600)), orderBy); + Assert.That (matches.Count, Is.EqualTo (14), "Unexpected COUNT"); + Assert.That (matches.Max.Value.Id, Is.EqualTo (14), "Unexpected MAX"); + Assert.That (matches.Min.Value.Id, Is.EqualTo (1), "Unexpected MIN"); + Assert.That (matches.UniqueIds, Has.Count.EqualTo (14), "Unexpected number of UIDs"); + for (int i = 0; i < matches.UniqueIds.Count; i++) + Assert.That (matches.UniqueIds[i].Id, Is.EqualTo (i + 1)); + Assert.That (matches.Relevancy, Has.Count.EqualTo (matches.Count), "Unexpected number of relevancy scores"); + + client.Capabilities &= ~ImapCapabilities.ESearch; + matches = ((ImapFolder) destination).Search ("ALL"); + Assert.That (matches.Max.HasValue, Is.True, "MAX should always be set"); + Assert.That (matches.Max.Value.Id, Is.EqualTo (14), "Unexpected MAX value"); + Assert.That (matches.Min.HasValue, Is.True, "MIN should always be set"); + Assert.That (matches.Min.Value.Id, Is.EqualTo (1), "Unexpected MIN value"); + Assert.That (matches.Count, Is.EqualTo (14), "COUNT should always be set"); + Assert.That (matches.UniqueIds, Has.Count.EqualTo (14)); + for (int i = 0; i < matches.UniqueIds.Count; i++) + Assert.That (matches.UniqueIds[i].Id, Is.EqualTo (i + 1)); + + client.Capabilities &= ~ImapCapabilities.ESort; + matches = ((ImapFolder) destination).Sort ("(REVERSE ARRIVAL) US-ASCII ALL"); + Assert.That (matches.Max.HasValue, Is.True, "MAX should always be set"); + Assert.That (matches.Max.Value.Id, Is.EqualTo (14), "Unexpected MAX value"); + Assert.That (matches.Min.HasValue, Is.True, "MIN should always be set"); + Assert.That (matches.Min.Value.Id, Is.EqualTo (1), "Unexpected MIN value"); + Assert.That (matches.Count, Is.EqualTo (14), "COUNT should always be set"); + Assert.That (matches.UniqueIds, Has.Count.EqualTo (14)); + var expectedSortByReverseArrivalResults = new uint[] { 7, 14, 6, 13, 5, 12, 4, 11, 3, 10, 2, 9, 1, 8 }; + for (int i = 0; i < matches.UniqueIds.Count; i++) + Assert.That (matches.UniqueIds[i].Id, Is.EqualTo (expectedSortByReverseArrivalResults[i])); + + destination.GetStreams (UniqueIdRange.All, GetStreamsCallback); + destination.GetStreams (new int[] { 0, 1, 2 }, GetStreamsCallback); + destination.GetStreams (0, -1, GetStreamsCallback); + + destination.Expunge (); + Assert.That (destination.HighestModSeq, Is.EqualTo (7)); + Assert.That (vanished, Has.Count.EqualTo (1), "Unexpected number of Vanished events"); + Assert.That (vanished[0].UniqueIds, Has.Count.EqualTo (14), "Unexpected number of UIDs in Vanished event"); + for (int i = 0; i < vanished[0].UniqueIds.Count; i++) + Assert.That (vanished[0].UniqueIds[i].Id, Is.EqualTo (i + 1)); + Assert.That (vanished[0].Earlier, Is.False, "Unexpected value for Earlier"); + vanished.Clear (); + + destination.Close (true); + + int alerts = 0; + client.Alert += (sender, e) => { + Assert.That (e.Message, Is.EqualTo ("System shutdown in 10 minutes")); + alerts++; + }; + client.NoOp (); + Assert.That (alerts, Is.EqualTo (1), "Alert event failed to fire."); + + client.Disconnect (true); + } + } + + [Test] + public async Task TestDovecotAsync () + { + var expectedFlags = MessageFlags.Answered | MessageFlags.Flagged | MessageFlags.Deleted | MessageFlags.Seen | MessageFlags.Draft; + var expectedPermanentFlags = expectedFlags | MessageFlags.UserDefined; + var commands = CreateDovecotCommands (out var internalDates, out var messages, out var flags); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + + Assert.That (client.Capabilities, Is.EqualTo (DovecotInitialCapabilities)); + Assert.That (client.AuthenticationMechanisms, Has.Count.EqualTo (4)); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN"), "Expected SASL PLAIN auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("DIGEST-MD5"), "Expected SASL DIGEST-MD5 auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("CRAM-MD5"), "Expected SASL CRAM-MD5 auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("NTLM"), "Expected SASL NTLM auth mechanism"); + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities, Is.EqualTo (DovecotAuthenticatedCapabilities)); + Assert.That (client.InternationalizationLevel, Is.EqualTo (1), "Expected I18NLEVEL=1"); + Assert.That (client.ThreadingAlgorithms, Does.Contain (ThreadingAlgorithm.OrderedSubject), "Expected THREAD=ORDEREDSUBJECT"); + Assert.That (client.ThreadingAlgorithms, Does.Contain (ThreadingAlgorithm.References), "Expected THREAD=REFERENCES"); + // TODO: verify CONTEXT=SEARCH + + var personal = client.GetFolder (client.PersonalNamespaces[0]); + + // Make sure these all throw NotSupportedException + Assert.ThrowsAsync (async () => await client.EnableUTF8Async ()); + Assert.ThrowsAsync (async () => await client.Inbox.GetAccessRightsAsync ("smith")); + Assert.ThrowsAsync (async () => await client.Inbox.GetMyAccessRightsAsync ()); + var rights = new AccessRights ("lrswida"); + Assert.ThrowsAsync (async () => await client.Inbox.AddAccessRightsAsync ("smith", rights)); + Assert.ThrowsAsync (async () => await client.Inbox.RemoveAccessRightsAsync ("smith", rights)); + Assert.ThrowsAsync (async () => await client.Inbox.SetAccessRightsAsync ("smith", rights)); + Assert.ThrowsAsync (async () => await client.Inbox.RemoveAccessAsync ("smith")); + Assert.ThrowsAsync (async () => await client.Inbox.GetQuotaAsync ()); + Assert.ThrowsAsync (async () => await client.Inbox.SetQuotaAsync (null, null)); + Assert.ThrowsAsync (async () => await client.GetMetadataAsync (MetadataTag.PrivateComment)); + Assert.ThrowsAsync (async () => await client.GetMetadataAsync (new MetadataTag[] { MetadataTag.PrivateComment })); + Assert.ThrowsAsync (async () => await client.SetMetadataAsync (new MetadataCollection ())); var labels = new string[] { "Label1", "Label2" }; - Assert.Throws (() => client.Inbox.AddLabels (UniqueId.MinValue, labels, true)); - Assert.Throws (() => client.Inbox.AddLabels (UniqueIdRange.All, labels, true)); - Assert.Throws (() => client.Inbox.AddLabels (UniqueIdRange.All, 1, labels, true)); - Assert.Throws (() => client.Inbox.AddLabels (0, labels, true)); - Assert.Throws (() => client.Inbox.AddLabels (new int[] { 0 }, labels, true)); - Assert.Throws (() => client.Inbox.AddLabels (new int[] { 0 }, 1, labels, true)); - Assert.Throws (() => client.Inbox.RemoveLabels (UniqueId.MinValue, labels, true)); - Assert.Throws (() => client.Inbox.RemoveLabels (UniqueIdRange.All, labels, true)); - Assert.Throws (() => client.Inbox.RemoveLabels (UniqueIdRange.All, 1, labels, true)); - Assert.Throws (() => client.Inbox.RemoveLabels (0, labels, true)); - Assert.Throws (() => client.Inbox.RemoveLabels (new int[] { 0 }, labels, true)); - Assert.Throws (() => client.Inbox.RemoveLabels (new int[] { 0 }, 1, labels, true)); - Assert.Throws (() => client.Inbox.SetLabels (UniqueId.MinValue, labels, true)); - Assert.Throws (() => client.Inbox.SetLabels (UniqueIdRange.All, labels, true)); - Assert.Throws (() => client.Inbox.SetLabels (UniqueIdRange.All, 1, labels, true)); - Assert.Throws (() => client.Inbox.SetLabels (0, labels, true)); - Assert.Throws (() => client.Inbox.SetLabels (new int[] { 0 }, labels, true)); - Assert.Throws (() => client.Inbox.SetLabels (new int[] { 0 }, 1, labels, true)); + Assert.ThrowsAsync (async () => await client.Inbox.AddLabelsAsync (UniqueId.MinValue, labels, true)); + Assert.ThrowsAsync (async () => await client.Inbox.AddLabelsAsync (UniqueIdRange.All, labels, true)); + Assert.ThrowsAsync (async () => await client.Inbox.AddLabelsAsync (UniqueIdRange.All, 1, labels, true)); + Assert.ThrowsAsync (async () => await client.Inbox.AddLabelsAsync (0, labels, true)); + Assert.ThrowsAsync (async () => await client.Inbox.AddLabelsAsync (new int[] { 0 }, labels, true)); + Assert.ThrowsAsync (async () => await client.Inbox.AddLabelsAsync (new int[] { 0 }, 1, labels, true)); + Assert.ThrowsAsync (async () => await client.Inbox.RemoveLabelsAsync (UniqueId.MinValue, labels, true)); + Assert.ThrowsAsync (async () => await client.Inbox.RemoveLabelsAsync (UniqueIdRange.All, labels, true)); + Assert.ThrowsAsync (async () => await client.Inbox.RemoveLabelsAsync (UniqueIdRange.All, 1, labels, true)); + Assert.ThrowsAsync (async () => await client.Inbox.RemoveLabelsAsync (0, labels, true)); + Assert.ThrowsAsync (async () => await client.Inbox.RemoveLabelsAsync (new int[] { 0 }, labels, true)); + Assert.ThrowsAsync (async () => await client.Inbox.RemoveLabelsAsync (new int[] { 0 }, 1, labels, true)); + Assert.ThrowsAsync (async () => await client.Inbox.SetLabelsAsync (UniqueId.MinValue, labels, true)); + Assert.ThrowsAsync (async () => await client.Inbox.SetLabelsAsync (UniqueIdRange.All, labels, true)); + Assert.ThrowsAsync (async () => await client.Inbox.SetLabelsAsync (UniqueIdRange.All, 1, labels, true)); + Assert.ThrowsAsync (async () => await client.Inbox.SetLabelsAsync (0, labels, true)); + Assert.ThrowsAsync (async () => await client.Inbox.SetLabelsAsync (new int[] { 0 }, labels, true)); + Assert.ThrowsAsync (async () => await client.Inbox.SetLabelsAsync (new int[] { 0 }, 1, labels, true)); + + try { + await client.EnableQuickResyncAsync (); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception when enabling QRESYNC: {ex}"); + } + + // take advantage of LIST-STATUS to get top-level personal folders... + var statusItems = StatusItems.Count | StatusItems.HighestModSeq | StatusItems.Recent | StatusItems.UidNext | StatusItems.UidValidity | StatusItems.Unread; + + var folders = (await personal.GetSubfoldersAsync (statusItems, false)).ToArray (); + Assert.That (folders, Has.Length.EqualTo (7), "Expected 7 folders"); + + var expectedFolderNames = new [] { "Archives", "Drafts", "Junk", "Sent Messages", "Trash", "INBOX", "NIL" }; + var expectedUidValidities = new [] { 1436832059, 1436832060, 1436832061, 1436832062, 1436832063, 1436832057, 1436832057 }; + var expectedHighestModSeq = new [] { 1, 1, 1, 1, 1, 15, 1 }; + var expectedMessages = new [] { 0, 0, 0, 0, 0, 4, 0 }; + var expectedUidNext = new [] { 1, 1, 1, 1, 1, 5, 1 }; + var expectedRecent = new [] { 0, 0, 0, 0, 0, 0, 0 }; + var expectedUnseen = new [] { 0, 0, 0, 0, 0, 0, 0 }; + + for (int i = 0; i < folders.Length; i++) { + Assert.That (folders[i].FullName, Is.EqualTo (expectedFolderNames[i]), "FullName did not match"); + Assert.That (folders[i].Name, Is.EqualTo (expectedFolderNames[i]), "Name did not match"); + Assert.That (folders[i].UidValidity, Is.EqualTo (expectedUidValidities[i]), "UidValidity did not match"); + Assert.That (folders[i].HighestModSeq, Is.EqualTo (expectedHighestModSeq[i]), "HighestModSeq did not match"); + Assert.That (folders[i], Has.Count.EqualTo (expectedMessages[i]), "Count did not match"); + Assert.That (folders[i].Recent, Is.EqualTo (expectedRecent[i]), "Recent did not match"); + Assert.That (folders[i].Unread, Is.EqualTo (expectedUnseen[i]), "Unread did not match"); + } + + var unitTests = await personal.CreateAsync ("UnitTests", false); + Assert.That (unitTests.Attributes, Is.EqualTo (FolderAttributes.HasNoChildren), "Unexpected UnitTests folder attributes"); + + var folder = await unitTests.CreateAsync ("Messages", true); + Assert.That (folder.Attributes, Is.EqualTo (FolderAttributes.HasNoChildren), "Unexpected UnitTests.Messages folder attributes"); + //Assert.That (unitTests.Attributes, Is.EqualTo (FolderAttributes.HasChildren), "Expected UnitTests Attributes to be updated"); + + // Use MULTIAPPEND to append some test messages + var appended = await folder.AppendAsync (messages, flags, internalDates); + Assert.That (appended, Has.Count.EqualTo (8), "Unexpected number of messages appended"); + foreach (var message in messages) + message.Dispose (); + + // SELECT the folder so that we can test some stuff + var access = await folder.OpenAsync (FolderAccess.ReadWrite); + Assert.That (folder.PermanentFlags, Is.EqualTo (expectedPermanentFlags), "UnitTests.Messages PERMANENTFLAGS"); + Assert.That (folder.AcceptedFlags, Is.EqualTo (expectedFlags), "UnitTests.Messages FLAGS"); + Assert.That (folder, Has.Count.EqualTo (8), "UnitTests.Messages EXISTS"); + Assert.That (folder.Recent, Is.EqualTo (8), "UnitTests.Messages RECENT"); + Assert.That (folder.FirstUnread, Is.EqualTo (0), "UnitTests.Messages UNSEEN"); + Assert.That (folder.UidValidity, Is.EqualTo (1436832084U), "UnitTests.Messages UIDVALIDITY"); + Assert.That (folder.UidNext.Value.Id, Is.EqualTo (9), "UnitTests.Messages UIDNEXT"); + Assert.That (folder.HighestModSeq, Is.EqualTo (2UL), "UnitTests.Messages HIGHESTMODSEQ"); + Assert.That (access, Is.EqualTo (FolderAccess.ReadWrite), "Expected UnitTests.Messages to be opened in READ-WRITE mode"); + + // Keep track of various folder events + var flagsChanged = new List (); + var modSeqChanged = new List (); + var vanished = new List (); + bool recentChanged = false; + + folder.MessageFlagsChanged += (sender, e) => { + flagsChanged.Add (e); + }; + + folder.ModSeqChanged += (sender, e) => { + modSeqChanged.Add (e); + }; + + folder.MessagesVanished += (sender, e) => { + vanished.Add (e); + }; + + folder.RecentChanged += (sender, e) => { + recentChanged = true; + }; + + // Keep track of UIDVALIDITY and HIGHESTMODSEQ values for our QRESYNC test later + var highestModSeq = folder.HighestModSeq; + var uidValidity = folder.UidValidity; + + // Make some FLAGS changes to our messages so we can test QRESYNC + await folder.AddFlagsAsync (appended, MessageFlags.Seen, true); + Assert.That (flagsChanged, Is.Empty, "Unexpected number of FlagsChanged events"); + Assert.That (modSeqChanged, Has.Count.EqualTo (8), "Unexpected number of ModSeqChanged events"); + for (int i = 0; i < modSeqChanged.Count; i++) { + Assert.That (modSeqChanged[i].Index, Is.EqualTo (i), $"Unexpected modSeqChanged[{i}].Index"); + Assert.That (modSeqChanged[i].UniqueId.Value.Id, Is.EqualTo (i + 1), $"Unexpected modSeqChanged[{i}].UniqueId"); + Assert.That (modSeqChanged[i].ModSeq, Is.EqualTo (3), $"Unexpected modSeqChanged[{i}].ModSeq"); + } + Assert.That (recentChanged, Is.False, "Unexpected RecentChanged event"); + modSeqChanged.Clear (); + flagsChanged.Clear (); + + var answered = new UniqueIdSet (SortOrder.Ascending) { + appended[0], // A + appended[1], // B + appended[2] // C + }; + await folder.AddFlagsAsync (answered, MessageFlags.Answered, true); + Assert.That (flagsChanged, Is.Empty, "Unexpected number of FlagsChanged events"); + Assert.That (modSeqChanged, Has.Count.EqualTo (3), "Unexpected number of ModSeqChanged events"); + for (int i = 0; i < modSeqChanged.Count; i++) { + Assert.That (modSeqChanged[i].Index, Is.EqualTo (i), $"Unexpected modSeqChanged[{i}].Index"); + Assert.That (modSeqChanged[i].UniqueId.Value.Id, Is.EqualTo (i + 1), $"Unexpected modSeqChanged[{i}].UniqueId"); + Assert.That (modSeqChanged[i].ModSeq, Is.EqualTo (4), $"Unexpected modSeqChanged[{i}].ModSeq"); + } + Assert.That (recentChanged, Is.False, "Unexpected RecentChanged event"); + modSeqChanged.Clear (); + flagsChanged.Clear (); + + // Delete some messages so we can test that QRESYNC emits some MessageVanished events + // both now *and* when we use QRESYNC to re-open the folder + var deleted = new UniqueIdSet (SortOrder.Ascending) { + appended[7] // H + }; + await folder.AddFlagsAsync (deleted, MessageFlags.Deleted, true); + Assert.That (flagsChanged, Is.Empty, "Unexpected number of FlagsChanged events"); + Assert.That (modSeqChanged, Has.Count.EqualTo (1), "Unexpected number of ModSeqChanged events"); + Assert.That (modSeqChanged[0].Index, Is.EqualTo (7), $"Unexpected modSeqChanged[{0}].Index"); + Assert.That (modSeqChanged[0].UniqueId.Value.Id, Is.EqualTo (8), $"Unexpected modSeqChanged[{0}].UniqueId"); + Assert.That (modSeqChanged[0].ModSeq, Is.EqualTo (5), $"Unexpected modSeqChanged[{0}].ModSeq"); + Assert.That (recentChanged, Is.False, "Unexpected RecentChanged event"); + modSeqChanged.Clear (); + flagsChanged.Clear (); + + await folder.ExpungeAsync (deleted); + Assert.That (vanished, Has.Count.EqualTo (1), "Expected MessagesVanished event"); + Assert.That (vanished[0].UniqueIds, Has.Count.EqualTo (1), "Unexpected number of messages vanished"); + Assert.That (vanished[0].UniqueIds[0].Id, Is.EqualTo (8), "Unexpected UID for vanished message"); + Assert.That (vanished[0].Earlier, Is.False, "Expected EARLIER to be false"); + Assert.That (recentChanged, Is.True, "Expected RecentChanged event"); + recentChanged = false; + vanished.Clear (); + + // Verify that THREAD works correctly + var threaded = await folder.ThreadAsync (ThreadingAlgorithm.References, SearchQuery.All); + Assert.That (threaded, Has.Count.EqualTo (2), "Unexpected number of root nodes in threaded results"); + + threaded = await folder.ThreadAsync (UniqueIdRange.All, ThreadingAlgorithm.OrderedSubject, SearchQuery.All); + Assert.That (threaded, Has.Count.EqualTo (7), "Unexpected number of root nodes in threaded results"); + + // UNSELECT the folder so we can re-open it using QRESYNC + await folder.CloseAsync (); + + // Use QRESYNC to get the changes since last time we opened the folder + access = await folder.OpenAsync (FolderAccess.ReadWrite, uidValidity, highestModSeq, appended); + Assert.That (access, Is.EqualTo (FolderAccess.ReadWrite), "Expected UnitTests.Messages to be opened in READ-WRITE mode"); + Assert.That (flagsChanged, Has.Count.EqualTo (7), "Unexpected number of MessageFlagsChanged events"); + Assert.That (modSeqChanged, Has.Count.EqualTo (7), "Unexpected number of ModSeqChanged events"); + for (int i = 0; i < flagsChanged.Count; i++) { + var messageFlags = MessageFlags.Seen | MessageFlags.Draft; + + if (i < 3) + messageFlags |= MessageFlags.Answered; + + Assert.That (flagsChanged[i].Index, Is.EqualTo (i), $"Unexpected value for flagsChanged[{i}].Index"); + Assert.That (flagsChanged[i].UniqueId.Value.Id, Is.EqualTo ((uint) (i + 1)), $"Unexpected value for flagsChanged[{i}].UniqueId"); + Assert.That (flagsChanged[i].Flags, Is.EqualTo (messageFlags), $"Unexpected value for flagsChanged[{i}].Flags"); + + Assert.That (modSeqChanged[i].Index, Is.EqualTo (i), $"Unexpected value for modSeqChanged[{i}].Index"); + if (i < 3) + Assert.That (modSeqChanged[i].ModSeq, Is.EqualTo (4), $"Unexpected value for modSeqChanged[{i}].ModSeq"); + else + Assert.That (modSeqChanged[i].ModSeq, Is.EqualTo (3), $"Unexpected value for modSeqChanged[{i}].ModSeq"); + } + modSeqChanged.Clear (); + flagsChanged.Clear (); + + Assert.That (vanished, Has.Count.EqualTo (1), "Unexpected number of MessagesVanished events"); + Assert.That (vanished[0].Earlier, Is.True, "Expected VANISHED EARLIER"); + Assert.That (vanished[0].UniqueIds, Has.Count.EqualTo (1), "Unexpected number of messages vanished"); + Assert.That (vanished[0].UniqueIds[0].Id, Is.EqualTo (8), "Unexpected UID for vanished message"); + vanished.Clear (); + + Assert.Throws (() => folder.Search (SearchQuery.GMailMessageId (1))); + Assert.Throws (() => folder.Search (SearchQuery.GMailThreadId (1))); + Assert.Throws (() => folder.Search (SearchQuery.HasGMailLabel ("Custom Label"))); + Assert.Throws (() => folder.Search (SearchQuery.GMailRawSearch ("has:attachment in:unread"))); + Assert.Throws (() => folder.Search (SearchQuery.Fuzzy (SearchQuery.SubjectContains ("some fuzzy text")))); + Assert.Throws (() => folder.Search (SearchQuery.Filter (new MetadataTag ("/private/filters/values/saved-search")))); + Assert.Throws (() => folder.Search (SearchQuery.Filter ("saved-search"))); + Assert.Throws (() => folder.Search (SearchQuery.SaveDateSupported)); + Assert.Throws (() => folder.Search (SearchQuery.SavedBefore (DateTime.Now))); + Assert.Throws (() => folder.Search (SearchQuery.SavedOn (DateTime.Now))); + Assert.Throws (() => folder.Search (SearchQuery.SavedSince (DateTime.Now))); + + // Use SEARCH and FETCH to get the same info + var searchOptions = SearchOptions.All | SearchOptions.Count | SearchOptions.Min | SearchOptions.Max | SearchOptions.Relevancy; + var changed = await folder.SearchAsync (searchOptions, SearchQuery.ChangedSince (highestModSeq)); + Assert.That (changed.UniqueIds, Has.Count.EqualTo (7), "Unexpected number of UIDs"); + Assert.That (changed.Relevancy, Has.Count.EqualTo (changed.Count), "Unexpected number of relevancy scores"); + Assert.That (changed.ModSeq.HasValue, Is.True, "Expected the ModSeq property to be set"); + Assert.That (changed.ModSeq.Value, Is.EqualTo (4), "Unexpected ModSeq value"); + Assert.That (changed.Min.Value.Id, Is.EqualTo (1), "Unexpected Min"); + Assert.That (changed.Max.Value.Id, Is.EqualTo (7), "Unexpected Max"); + Assert.That (changed.Count, Is.EqualTo (7), "Unexpected Count"); + + var fetched = await folder.FetchAsync (changed.UniqueIds, MessageSummaryItems.UniqueId | MessageSummaryItems.Flags | MessageSummaryItems.ModSeq); + Assert.That (fetched, Has.Count.EqualTo (7), "Unexpected number of messages fetched"); + for (int i = 0; i < fetched.Count; i++) { + Assert.That (fetched[i].Index, Is.EqualTo (i), "Unexpected Index"); + Assert.That (fetched[i].UniqueId.Id, Is.EqualTo (i + 1), "Unexpected UniqueId"); + } + + // or... we could just use a single UID FETCH command like so: + fetched = await folder.FetchAsync (UniqueIdRange.All, highestModSeq, MessageSummaryItems.UniqueId | MessageSummaryItems.Flags | MessageSummaryItems.ModSeq); + for (int i = 0; i < fetched.Count; i++) { + Assert.That (fetched[i].Index, Is.EqualTo (i), "Unexpected Index"); + Assert.That (fetched[i].UniqueId.Id, Is.EqualTo (i + 1), "Unexpected UniqueId"); + } + Assert.That (fetched, Has.Count.EqualTo (7), "Unexpected number of messages fetched"); + Assert.That (vanished, Has.Count.EqualTo (1), "Unexpected number of MessagesVanished events"); + Assert.That (vanished[0].Earlier, Is.True, "Expected VANISHED EARLIER"); + Assert.That (vanished[0].UniqueIds, Has.Count.EqualTo (1), "Unexpected number of messages vanished"); + Assert.That (vanished[0].UniqueIds[0].Id, Is.EqualTo (8), "Unexpected UID for vanished message"); + vanished.Clear (); + + // Use SORT to order by reverse arrival order + var orderBy = new OrderBy[] { new OrderBy (OrderByType.Arrival, SortOrder.Descending) }; + var sorted = await folder.SortAsync (searchOptions, SearchQuery.All, orderBy); + Assert.That (sorted.UniqueIds, Has.Count.EqualTo (7), "Unexpected number of UIDs"); + for (int i = 0; i < sorted.UniqueIds.Count; i++) + Assert.That (sorted.UniqueIds[i].Id, Is.EqualTo (7 - i), $"Unexpected value for UniqueId[{i}]"); + Assert.That (sorted.Relevancy, Has.Count.EqualTo (sorted.Count), "Unexpected number of relevancy scores"); + Assert.That (sorted.ModSeq.HasValue, Is.False, "Expected the ModSeq property to be null"); + Assert.That (sorted.Min.Value.Id, Is.EqualTo (7), "Unexpected Min"); + Assert.That (sorted.Max.Value.Id, Is.EqualTo (1), "Unexpected Max"); + Assert.That (sorted.Count, Is.EqualTo (7), "Unexpected Count"); + + // Verify that optimizing NOT queries works correctly + var uids = await folder.SearchAsync (SearchQuery.Not (SearchQuery.Deleted).And (SearchQuery.Not (SearchQuery.NotSeen))); + Assert.That (uids, Has.Count.EqualTo (7), "Unexpected number of UIDs"); + for (int i = 0; i < uids.Count; i++) + Assert.That (uids[i].Id, Is.EqualTo (i + 1), $"Unexpected value for uids[{i}]"); + + // Create a Destination folder to use for copying/moving messages to + var destination = (ImapFolder) await unitTests.CreateAsync ("Destination", true); + Assert.That (destination.Attributes, Is.EqualTo (FolderAttributes.HasNoChildren), "Unexpected UnitTests.Destination folder attributes"); + + // COPY messages to the Destination folder + var copied = await folder.CopyToAsync (uids, destination); + Assert.That (copied.Source, Has.Count.EqualTo (uids.Count), "Unexpected Source.Count"); + Assert.That (copied.Destination, Has.Count.EqualTo (uids.Count), "Unexpected Destination.Count"); + + // MOVE messages to the Destination folder + var moved = await folder.MoveToAsync (uids, destination); + Assert.That (copied.Source, Has.Count.EqualTo (uids.Count), "Unexpected Source.Count"); + Assert.That (copied.Destination, Has.Count.EqualTo (uids.Count), "Unexpected Destination.Count"); + Assert.That (vanished, Has.Count.EqualTo (1), "Expected VANISHED event"); + vanished.Clear (); + + await destination.StatusAsync (statusItems); + Assert.That (destination.UidValidity, Is.EqualTo (moved.Destination[0].Validity), "Unexpected UIDVALIDITY"); + + destination.MessageFlagsChanged += (sender, e) => { + flagsChanged.Add (e); + }; + + destination.ModSeqChanged += (sender, e) => { + modSeqChanged.Add (e); + }; + + destination.MessagesVanished += (sender, e) => { + vanished.Add (e); + }; + + destination.RecentChanged += (sender, e) => { + recentChanged = true; + }; + + await destination.OpenAsync (FolderAccess.ReadWrite); + Assert.That (access, Is.EqualTo (FolderAccess.ReadWrite), "Expected UnitTests.Destination to be opened in READ-WRITE mode"); + + var fetchHeaders = new HashSet { + HeaderId.References, + HeaderId.XMailer + }; + + var indexes = new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13 }; + + // Fetch + modseq + fetched = await destination.FetchAsync (UniqueIdRange.All, 1, MessageSummaryItems.Full | MessageSummaryItems.UniqueId | + MessageSummaryItems.BodyStructure | MessageSummaryItems.ModSeq | + MessageSummaryItems.References, fetchHeaders); + Assert.That (fetched, Has.Count.EqualTo (14), "Unexpected number of messages fetched"); + + fetched = await destination.FetchAsync (0, -1, 1, MessageSummaryItems.Full | MessageSummaryItems.UniqueId | + MessageSummaryItems.BodyStructure | MessageSummaryItems.ModSeq | + MessageSummaryItems.References, fetchHeaders); + Assert.That (fetched, Has.Count.EqualTo (14), "Unexpected number of messages fetched"); + + fetched = await destination.FetchAsync (indexes, 1, MessageSummaryItems.Full | MessageSummaryItems.UniqueId | + MessageSummaryItems.BodyStructure | MessageSummaryItems.ModSeq | + MessageSummaryItems.References, fetchHeaders); + Assert.That (fetched, Has.Count.EqualTo (14), "Unexpected number of messages fetched"); + + fetched = await destination.FetchAsync (0, -1, 1, MessageSummaryItems.Full | MessageSummaryItems.UniqueId | + MessageSummaryItems.BodyStructure | MessageSummaryItems.ModSeq | + MessageSummaryItems.References); + Assert.That (fetched, Has.Count.EqualTo (14), "Unexpected number of messages fetched"); + + fetched = await destination.FetchAsync (indexes, 1, MessageSummaryItems.Full | MessageSummaryItems.UniqueId | + MessageSummaryItems.BodyStructure | MessageSummaryItems.ModSeq | + MessageSummaryItems.References); + Assert.That (fetched, Has.Count.EqualTo (14), "Unexpected number of messages fetched"); + + // Fetch + fetched = await destination.FetchAsync (UniqueIdRange.All, MessageSummaryItems.Full | MessageSummaryItems.UniqueId | + MessageSummaryItems.BodyStructure | MessageSummaryItems.ModSeq | + MessageSummaryItems.References, fetchHeaders); + Assert.That (fetched, Has.Count.EqualTo (14), "Unexpected number of messages fetched"); + + fetched = await destination.FetchAsync (0, -1, MessageSummaryItems.Full | MessageSummaryItems.UniqueId | + MessageSummaryItems.BodyStructure | MessageSummaryItems.ModSeq | + MessageSummaryItems.References, fetchHeaders); + Assert.That (fetched, Has.Count.EqualTo (14), "Unexpected number of messages fetched"); + + fetched = await destination.FetchAsync (indexes, MessageSummaryItems.Full | MessageSummaryItems.UniqueId | + MessageSummaryItems.BodyStructure | MessageSummaryItems.ModSeq | + MessageSummaryItems.References, fetchHeaders); + Assert.That (fetched, Has.Count.EqualTo (14), "Unexpected number of messages fetched"); + + fetched = await destination.FetchAsync (0, -1, MessageSummaryItems.Full | MessageSummaryItems.UniqueId | + MessageSummaryItems.BodyStructure | MessageSummaryItems.ModSeq | + MessageSummaryItems.References); + Assert.That (fetched, Has.Count.EqualTo (14), "Unexpected number of messages fetched"); + + fetched = await destination.FetchAsync (indexes, MessageSummaryItems.Full | MessageSummaryItems.UniqueId | + MessageSummaryItems.BodyStructure | MessageSummaryItems.ModSeq | + MessageSummaryItems.References); + Assert.That (fetched, Has.Count.EqualTo (14), "Unexpected number of messages fetched"); + + uids = new UniqueIdSet (SortOrder.Ascending); + + for (int i = 0; i < fetched.Count; i++) { + Assert.That (fetched[i].Index, Is.EqualTo (i), "Unexpected Index"); + Assert.That (fetched[i].UniqueId.Id, Is.EqualTo (i + 1), "Unexpected UniqueId"); + + uids.Add (fetched[i].UniqueId); + } + + using (var entity = await destination.GetBodyPartAsync (fetched[0].UniqueId, fetched[0].TextBody)) + Assert.That (entity, Is.InstanceOf ()); + + using (var entity = await destination.GetBodyPartAsync (fetched[0].Index, fetched[0].TextBody)) + Assert.That (entity, Is.InstanceOf ()); + + using (var entity = await destination.GetBodyPartAsync (fetched[1].UniqueId, fetched[1].TextBody)) + Assert.That (entity, Is.InstanceOf ()); + + using (var entity = await destination.GetBodyPartAsync (fetched[1].Index, fetched[1].TextBody)) + Assert.That (entity, Is.InstanceOf ()); + + var headers = await destination.GetHeadersAsync (fetched[0].UniqueId); + Assert.That (headers[HeaderId.From], Is.EqualTo ("Unit Tests "), "GetHeaders(UniqueId) failed to match From header"); + Assert.That (headers[HeaderId.Date], Is.EqualTo ("Sun, 02 Oct 2016 17:56:45 -0400"), "GetHeaders(UniqueId) failed to match Date header"); + Assert.That (headers[HeaderId.Subject], Is.EqualTo ("A"), "GetHeaders(UniqueId) failed to match Subject header"); + Assert.That (headers[HeaderId.MessageId], Is.EqualTo (""), "GetHeaders(UniqueId) failed to match Message-Id header"); + Assert.That (headers[HeaderId.To], Is.EqualTo ("Unit Tests "), "GetHeaders(UniqueId) failed to match To header"); + Assert.That (headers[HeaderId.MimeVersion], Is.EqualTo ("1.0"), "GetHeaders(UniqueId) failed to match MIME-Version header"); + Assert.That (headers[HeaderId.ContentType], Is.EqualTo ("text/plain; charset=utf-8"), "GetHeaders(UniqueId) failed to match Content-Type header"); + + headers = await destination.GetHeadersAsync (fetched[0].Index); + Assert.That (headers[HeaderId.From], Is.EqualTo ("Unit Tests "), "GetHeaders(int) failed to match From header"); + Assert.That (headers[HeaderId.Date], Is.EqualTo ("Sun, 02 Oct 2016 17:56:45 -0400"), "GetHeaders(UniqueId) failed to match Date header"); + Assert.That (headers[HeaderId.Subject], Is.EqualTo ("A"), "GetHeaders(UniqueId) failed to match Subject header"); + Assert.That (headers[HeaderId.MessageId], Is.EqualTo (""), "GetHeaders(UniqueId) failed to match Message-Id header"); + Assert.That (headers[HeaderId.To], Is.EqualTo ("Unit Tests "), "GetHeaders(UniqueId) failed to match To header"); + Assert.That (headers[HeaderId.MimeVersion], Is.EqualTo ("1.0"), "GetHeaders(UniqueId) failed to match MIME-Version header"); + Assert.That (headers[HeaderId.ContentType], Is.EqualTo ("text/plain; charset=utf-8"), "GetHeaders(UniqueId) failed to match Content-Type header"); + + headers = await destination.GetHeadersAsync (fetched[0].UniqueId, fetched[0].TextBody); + Assert.That (headers[HeaderId.From], Is.EqualTo ("Unit Tests "), "GetHeaders(UniqueId, BodyPart) failed to match From header"); + Assert.That (headers[HeaderId.Date], Is.EqualTo ("Sun, 02 Oct 2016 17:56:45 -0400"), "GetHeaders(UniqueId) failed to match Date header"); + Assert.That (headers[HeaderId.Subject], Is.EqualTo ("A"), "GetHeaders(UniqueId) failed to match Subject header"); + Assert.That (headers[HeaderId.MessageId], Is.EqualTo (""), "GetHeaders(UniqueId) failed to match Message-Id header"); + Assert.That (headers[HeaderId.To], Is.EqualTo ("Unit Tests "), "GetHeaders(UniqueId) failed to match To header"); + Assert.That (headers[HeaderId.MimeVersion], Is.EqualTo ("1.0"), "GetHeaders(UniqueId) failed to match MIME-Version header"); + Assert.That (headers[HeaderId.ContentType], Is.EqualTo ("text/plain; charset=utf-8"), "GetHeaders(UniqueId) failed to match Content-Type header"); + + headers = await destination.GetHeadersAsync (fetched[0].Index, fetched[0].TextBody); + Assert.That (headers[HeaderId.From], Is.EqualTo ("Unit Tests "), "GetHeaders(int, BodyPart) failed to match From header"); + Assert.That (headers[HeaderId.Date], Is.EqualTo ("Sun, 02 Oct 2016 17:56:45 -0400"), "GetHeaders(UniqueId) failed to match Date header"); + Assert.That (headers[HeaderId.Subject], Is.EqualTo ("A"), "GetHeaders(UniqueId) failed to match Subject header"); + Assert.That (headers[HeaderId.MessageId], Is.EqualTo (""), "GetHeaders(UniqueId) failed to match Message-Id header"); + Assert.That (headers[HeaderId.To], Is.EqualTo ("Unit Tests "), "GetHeaders(UniqueId) failed to match To header"); + Assert.That (headers[HeaderId.MimeVersion], Is.EqualTo ("1.0"), "GetHeaders(UniqueId) failed to match MIME-Version header"); + Assert.That (headers[HeaderId.ContentType], Is.EqualTo ("text/plain; charset=utf-8"), "GetHeaders(UniqueId) failed to match Content-Type header"); + + headers = await destination.GetHeadersAsync (fetched[1].UniqueId, fetched[1].TextBody); + Assert.That (headers[HeaderId.ContentType], Is.EqualTo ("text/plain; charset=utf-8"), "GetHeaders(UniqueId) failed to match Content-Type header"); + + headers = await destination.GetHeadersAsync (fetched[1].Index, fetched[1].TextBody); + Assert.That (headers[HeaderId.ContentType], Is.EqualTo ("text/plain; charset=utf-8"), "GetHeaders(UniqueId) failed to match Content-Type header"); + + using (var stream = await destination.GetStreamAsync (fetched[0].UniqueId, 128, 64)) { + Assert.That (stream.Length, Is.EqualTo (64), "Unexpected stream length"); + + string text; + using (var reader = new StreamReader (stream)) + text = reader.ReadToEnd (); + + Assert.That (text, Is.EqualTo ("nit Tests \r\nMIME-Version: 1.0\r\nContent-T")); + } + + using (var stream = await destination.GetStreamAsync (fetched[0].UniqueId, "", 128, 64)) { + Assert.That (stream.Length, Is.EqualTo (64), "Unexpected stream length"); + + string text; + using (var reader = new StreamReader (stream)) + text = reader.ReadToEnd (); + + Assert.That (text, Is.EqualTo ("nit Tests \r\nMIME-Version: 1.0\r\nContent-T")); + } + + using (var stream = await destination.GetStreamAsync (fetched[0].Index, 128, 64)) { + Assert.That (stream.Length, Is.EqualTo (64), "Unexpected stream length"); + + string text; + using (var reader = new StreamReader (stream)) + text = reader.ReadToEnd (); + + Assert.That (text, Is.EqualTo ("nit Tests \r\nMIME-Version: 1.0\r\nContent-T")); + } + + using (var stream = await destination.GetStreamAsync (fetched[0].Index, "", 128, 64)) { + Assert.That (stream.Length, Is.EqualTo (64), "Unexpected stream length"); + + string text; + using (var reader = new StreamReader (stream)) + text = reader.ReadToEnd (); + + Assert.That (text, Is.EqualTo ("nit Tests \r\nMIME-Version: 1.0\r\nContent-T")); + } + + using (var stream = await destination.GetStreamAsync (fetched[0].UniqueId, "HEADER.FIELDS (MIME-VERSION CONTENT-TYPE)")) { + Assert.That (stream.Length, Is.EqualTo (62), "Unexpected stream length"); + + string text; + using (var reader = new StreamReader (stream)) + text = reader.ReadToEnd (); + + Assert.That (text, Is.EqualTo ("MIME-Version: 1.0\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n")); + } + + using (var stream = await destination.GetStreamAsync (fetched[0].Index, "HEADER.FIELDS (MIME-VERSION CONTENT-TYPE)")) { + Assert.That (stream.Length, Is.EqualTo (62), "Unexpected stream length"); + + string text; + using (var reader = new StreamReader (stream)) + text = reader.ReadToEnd (); + + Assert.That (text, Is.EqualTo ("MIME-Version: 1.0\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n")); + } + + var custom = new HashSet { + "$MailKit" + }; + + var unchanged1 = await destination.AddFlagsAsync (uids, destination.HighestModSeq, MessageFlags.Deleted, custom, true); + Assert.That (modSeqChanged, Has.Count.EqualTo (14), "Unexpected number of ModSeqChanged events"); + Assert.That (destination.HighestModSeq, Is.EqualTo (5)); + for (int i = 0; i < modSeqChanged.Count; i++) { + Assert.That (modSeqChanged[i].Index, Is.EqualTo (i), $"Unexpected value for modSeqChanged[{i}].Index"); + Assert.That (modSeqChanged[i].ModSeq, Is.EqualTo (5), $"Unexpected value for modSeqChanged[{i}].ModSeq"); + } + Assert.That (unchanged1, Has.Count.EqualTo (2), "[MODIFIED uid-set]"); + Assert.That (unchanged1[0].Id, Is.EqualTo (7), "unchanged uids[0]"); + Assert.That (unchanged1[1].Id, Is.EqualTo (9), "unchanged uids[1]"); + modSeqChanged.Clear (); + + var unchanged2 = await destination.SetFlagsAsync (new int[] { 0, 1, 2, 3, 4, 5, 6 }, destination.HighestModSeq, MessageFlags.Seen | MessageFlags.Deleted, custom, true); + Assert.That (modSeqChanged, Has.Count.EqualTo (7), "Unexpected number of ModSeqChanged events"); + Assert.That (destination.HighestModSeq, Is.EqualTo (6)); + for (int i = 0; i < modSeqChanged.Count; i++) { + Assert.That (modSeqChanged[i].Index, Is.EqualTo (i), $"Unexpected value for modSeqChanged[{i}].Index"); + Assert.That (modSeqChanged[i].ModSeq, Is.EqualTo (6), $"Unexpected value for modSeqChanged[{i}].ModSeq"); + } + Assert.That (unchanged2, Has.Count.EqualTo (2), "[MODIFIED seq-set]"); + Assert.That (unchanged2[0], Is.EqualTo (6), "unchanged indexes[0]"); + Assert.That (unchanged2[1], Is.EqualTo (8), "unchanged indexes[1]"); + modSeqChanged.Clear (); + + var results = await destination.SearchAsync (uids, SearchQuery.New.Or (SearchQuery.Old.Or (SearchQuery.Answered.Or (SearchQuery.Deleted.Or (SearchQuery.Draft.Or (SearchQuery.Flagged.Or (SearchQuery.Recent.Or (SearchQuery.NotAnswered.Or (SearchQuery.NotDeleted.Or (SearchQuery.NotDraft.Or (SearchQuery.NotFlagged.Or (SearchQuery.NotSeen.Or (SearchQuery.HasKeyword ("$MailKit").Or (SearchQuery.NotKeyword ("$MailKit"))))))))))))))); + Assert.That (results, Has.Count.EqualTo (14), "Unexpected number of UIDs"); + + var matches = await destination.SearchAsync (searchOptions, uids, SearchQuery.LargerThan (256).And (SearchQuery.SmallerThan (512))); + var expectedMatchedUids = new uint[] { 2, 3, 4, 5, 6, 9, 10, 11, 12, 13 }; + Assert.That (matches.Count, Is.EqualTo (10), "Unexpected COUNT"); + Assert.That (matches.Max.Value.Id, Is.EqualTo (13), "Unexpected MAX"); + Assert.That (matches.Min.Value.Id, Is.EqualTo (2), "Unexpected MIN"); + Assert.That (matches.UniqueIds, Has.Count.EqualTo (10), "Unexpected number of UIDs"); + for (int i = 0; i < matches.UniqueIds.Count; i++) + Assert.That (matches.UniqueIds[i].Id, Is.EqualTo (expectedMatchedUids[i])); + Assert.That (matches.Relevancy, Has.Count.EqualTo (matches.Count), "Unexpected number of relevancy scores"); + + orderBy = new OrderBy[] { OrderBy.ReverseDate, OrderBy.Subject, OrderBy.DisplayFrom, OrderBy.Size }; + var sentDateQuery = SearchQuery.Or (SearchQuery.And (SearchQuery.SentBefore (new DateTime (2016, 10, 12)), SearchQuery.SentSince (new DateTime (2016, 10, 10))), SearchQuery.Not (SearchQuery.SentOn (new DateTime (2016, 10, 11)))); + var deliveredDateQuery = SearchQuery.Or (SearchQuery.And (SearchQuery.DeliveredBefore (new DateTime (2016, 10, 12)), SearchQuery.DeliveredAfter (new DateTime (2016, 10, 10))), SearchQuery.Not (SearchQuery.DeliveredOn (new DateTime (2016, 10, 11)))); + results = await destination.SortAsync (sentDateQuery.Or (deliveredDateQuery), orderBy); + var expectedSortByDateResults = new uint[] { 7, 14, 6, 13, 5, 12, 4, 11, 3, 10, 2, 9, 1, 8 }; + Assert.That (results, Has.Count.EqualTo (14), "Unexpected number of UIDs"); + for (int i = 0; i < results.Count; i++) + Assert.That (results[i].Id, Is.EqualTo (expectedSortByDateResults[i])); + + var stringQuery = SearchQuery.BccContains ("xyz").Or (SearchQuery.CcContains ("xyz").Or (SearchQuery.FromContains ("xyz").Or (SearchQuery.ToContains ("xyz").Or (SearchQuery.SubjectContains ("xyz").Or (SearchQuery.HeaderContains ("Message-Id", "mimekit.net").Or (SearchQuery.BodyContains ("This is the message body.").Or (SearchQuery.MessageContains ("message")))))))); + orderBy = new OrderBy[] { OrderBy.From, OrderBy.To, OrderBy.Cc }; + results = await destination.SortAsync (uids, stringQuery, orderBy); + Assert.That (results, Has.Count.EqualTo (14), "Unexpected number of UIDs"); + for (int i = 0; i < results.Count; i++) + Assert.That (results[i].Id, Is.EqualTo (i + 1)); + + orderBy = new OrderBy[] { OrderBy.DisplayTo }; + matches = await destination.SortAsync (searchOptions, uids, SearchQuery.OlderThan (1).And (SearchQuery.YoungerThan (3600)), orderBy); + Assert.That (matches.Count, Is.EqualTo (14), "Unexpected COUNT"); + Assert.That (matches.Max.Value.Id, Is.EqualTo (14), "Unexpected MAX"); + Assert.That (matches.Min.Value.Id, Is.EqualTo (1), "Unexpected MIN"); + Assert.That (matches.UniqueIds, Has.Count.EqualTo (14), "Unexpected number of UIDs"); + for (int i = 0; i < matches.UniqueIds.Count; i++) + Assert.That (matches.UniqueIds[i].Id, Is.EqualTo (i + 1)); + Assert.That (matches.Relevancy, Has.Count.EqualTo (matches.Count), "Unexpected number of relevancy scores"); + + client.Capabilities &= ~ImapCapabilities.ESearch; + matches = await ((ImapFolder) destination).SearchAsync ("ALL"); + Assert.That (matches.Max.HasValue, Is.True, "MAX should always be set"); + Assert.That (matches.Max.Value.Id, Is.EqualTo (14), "Unexpected MAX value"); + Assert.That (matches.Min.HasValue, Is.True, "MIN should always be set"); + Assert.That (matches.Min.Value.Id, Is.EqualTo (1), "Unexpected MIN value"); + Assert.That (matches.Count, Is.EqualTo (14), "COUNT should always be set"); + Assert.That (matches.UniqueIds, Has.Count.EqualTo (14)); + for (int i = 0; i < matches.UniqueIds.Count; i++) + Assert.That (matches.UniqueIds[i].Id, Is.EqualTo (i + 1)); + + client.Capabilities &= ~ImapCapabilities.ESort; + matches = await ((ImapFolder) destination).SortAsync ("(REVERSE ARRIVAL) US-ASCII ALL"); + Assert.That (matches.Max.HasValue, Is.True, "MAX should always be set"); + Assert.That (matches.Max.Value.Id, Is.EqualTo (14), "Unexpected MAX value"); + Assert.That (matches.Min.HasValue, Is.True, "MIN should always be set"); + Assert.That (matches.Min.Value.Id, Is.EqualTo (1), "Unexpected MIN value"); + Assert.That (matches.Count, Is.EqualTo (14), "COUNT should always be set"); + Assert.That (matches.UniqueIds, Has.Count.EqualTo (14)); + var expectedSortByReverseArrivalResults = new uint[] { 7, 14, 6, 13, 5, 12, 4, 11, 3, 10, 2, 9, 1, 8 }; + for (int i = 0; i < matches.UniqueIds.Count; i++) + Assert.That (matches.UniqueIds[i].Id, Is.EqualTo (expectedSortByReverseArrivalResults[i])); + + await destination.GetStreamsAsync (UniqueIdRange.All, GetStreamsAsyncCallback); + await destination.GetStreamsAsync (new int[] { 0, 1, 2 }, GetStreamsAsyncCallback); + await destination.GetStreamsAsync (0, -1, GetStreamsAsyncCallback); + + await destination.ExpungeAsync (); + Assert.That (destination.HighestModSeq, Is.EqualTo (7)); + Assert.That (vanished, Has.Count.EqualTo (1), "Unexpected number of Vanished events"); + Assert.That (vanished[0].UniqueIds, Has.Count.EqualTo (14), "Unexpected number of UIDs in Vanished event"); + for (int i = 0; i < vanished[0].UniqueIds.Count; i++) + Assert.That (vanished[0].UniqueIds[i].Id, Is.EqualTo (i + 1)); + Assert.That (vanished[0].Earlier, Is.False, "Unexpected value for Earlier"); + vanished.Clear (); + + await destination.CloseAsync (true); + + int alerts = 0; + client.Alert += (sender, e) => { + Assert.That (e.Message, Is.EqualTo ("System shutdown in 10 minutes")); + alerts++; + }; + await client.NoOpAsync (); + Assert.That (alerts, Is.EqualTo (1), "Alert event failed to fire."); + + await client.DisconnectAsync (true); + } + } + + static List CreateGMailCommands () + { + var commands = new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt"), + new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "gmail.authenticate.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-inbox.txt"), + new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt"), + new ImapReplayCommand ("A00000005 LIST \"\" \"%\"\r\n", "gmail.list-personal.txt"), + new ImapReplayCommand ("A00000006 CREATE UnitTests\r\n", ImapReplayCommandResponse.OK), + new ImapReplayCommand ("A00000007 LIST \"\" UnitTests\r\n", "gmail.list-unittests.txt"), + new ImapReplayCommand ("A00000008 SELECT UnitTests (CONDSTORE)\r\n", "gmail.select-unittests.txt") + }; + + for (int i = 0; i < 50; i++) { + MimeMessage message; + string latin1; + long length; + + using (var resource = GetResourceStream (string.Format ("common.message.{0}.msg", i))) + message = MimeMessage.Load (resource); + + using (var stream = new MemoryStream ()) { + var options = FormatOptions.Default.Clone (); + options.NewLineFormat = NewLineFormat.Dos; + options.EnsureNewLine = true; + + message.WriteTo (options, stream); + length = stream.Length; + stream.Position = 0; + + using (var reader = new StreamReader (stream, TextEncodings.Latin1)) + latin1 = reader.ReadToEnd (); + } + + message.Dispose (); + + var tag = string.Format ("A{0:D8}", i + 9); + var command = string.Format ("{0} APPEND UnitTests (\\Seen) ", tag); + + if (length > 4096) { + command += "{" + length + "}\r\n"; + commands.Add (new ImapReplayCommand (command, "gmail.go-ahead.txt")); + commands.Add (new ImapReplayCommand (tag, latin1 + "\r\n", string.Format ("gmail.append.{0}.txt", i + 1))); + } else { + command += "{" + length + "+}\r\n" + latin1 + "\r\n"; + commands.Add (new ImapReplayCommand (command, string.Format ("gmail.append.{0}.txt", i + 1))); + } + } + commands.Add (new ImapReplayCommand ("A00000059 UID SEARCH RETURN (ALL) OR X-GM-MSGID 1 OR X-GM-THRID 5 OR X-GM-LABELS \"Custom Label\" X-GM-RAW \"has:attachment in:unread\"\r\n", "gmail.search.txt")); + commands.Add (new ImapReplayCommand ("A00000060 UID FETCH 1:3,5,7:9,11:14,26:29,31,34,41:43,50 (UID FLAGS INTERNALDATE RFC822.SIZE ENVELOPE BODY X-GM-MSGID X-GM-THRID X-GM-LABELS)\r\n", "gmail.search-summary.txt")); + commands.Add (new ImapReplayCommand ("A00000061 UID FETCH 1 (BODY.PEEK[])\r\n", "gmail.fetch.1.txt")); + commands.Add (new ImapReplayCommand ("A00000062 UID FETCH 2 (BODY.PEEK[])\r\n", "gmail.fetch.2.txt")); + commands.Add (new ImapReplayCommand ("A00000063 UID FETCH 3 (BODY.PEEK[])\r\n", "gmail.fetch.3.txt")); + commands.Add (new ImapReplayCommand ("A00000064 UID FETCH 5 (BODY.PEEK[])\r\n", "gmail.fetch.5.txt")); + commands.Add (new ImapReplayCommand ("A00000065 UID FETCH 7 (BODY.PEEK[])\r\n", "gmail.fetch.7.txt")); + commands.Add (new ImapReplayCommand ("A00000066 UID FETCH 8 (BODY.PEEK[])\r\n", "gmail.fetch.8.txt")); + commands.Add (new ImapReplayCommand ("A00000067 UID FETCH 9 (BODY.PEEK[])\r\n", "gmail.fetch.9.txt")); + commands.Add (new ImapReplayCommand ("A00000068 UID FETCH 11 (BODY.PEEK[])\r\n", "gmail.fetch.11.txt")); + commands.Add (new ImapReplayCommand ("A00000069 UID FETCH 12 (BODY.PEEK[])\r\n", "gmail.fetch.12.txt")); + commands.Add (new ImapReplayCommand ("A00000070 UID FETCH 13 (BODY.PEEK[])\r\n", "gmail.fetch.13.txt")); + commands.Add (new ImapReplayCommand ("A00000071 UID FETCH 14 (BODY.PEEK[])\r\n", "gmail.fetch.14.txt")); + commands.Add (new ImapReplayCommand ("A00000072 UID FETCH 26 (BODY.PEEK[])\r\n", "gmail.fetch.26.txt")); + commands.Add (new ImapReplayCommand ("A00000073 UID FETCH 27 (BODY.PEEK[])\r\n", "gmail.fetch.27.txt")); + commands.Add (new ImapReplayCommand ("A00000074 UID FETCH 28 (BODY.PEEK[])\r\n", "gmail.fetch.28.txt")); + commands.Add (new ImapReplayCommand ("A00000075 UID FETCH 29 (BODY.PEEK[])\r\n", "gmail.fetch.29.txt")); + commands.Add (new ImapReplayCommand ("A00000076 UID FETCH 31 (BODY.PEEK[])\r\n", "gmail.fetch.31.txt")); + commands.Add (new ImapReplayCommand ("A00000077 UID FETCH 34 (BODY.PEEK[])\r\n", "gmail.fetch.34.txt")); + commands.Add (new ImapReplayCommand ("A00000078 UID FETCH 41 (BODY.PEEK[])\r\n", "gmail.fetch.41.txt")); + commands.Add (new ImapReplayCommand ("A00000079 UID FETCH 42 (BODY.PEEK[])\r\n", "gmail.fetch.42.txt")); + commands.Add (new ImapReplayCommand ("A00000080 UID FETCH 43 (BODY.PEEK[])\r\n", "gmail.fetch.43.txt")); + commands.Add (new ImapReplayCommand ("A00000081 UID FETCH 50 (BODY.PEEK[])\r\n", "gmail.fetch.50.txt")); + commands.Add (new ImapReplayCommand ("A00000082 UID STORE 1:3,5,7:9,11:14,26:29,31,34,41:43,50 X-GM-LABELS (\\Important \"Custom Label\" NIL)\r\n", "gmail.set-labels.txt")); + commands.Add (new ImapReplayCommand ("A00000083 UID STORE 1:3,5,7:9,11:14,26:29,31,34,41:43,50 -X-GM-LABELS.SILENT (\\Important \"Custom Label\" NIL)\r\n", ImapReplayCommandResponse.OK)); + commands.Add (new ImapReplayCommand ("A00000084 UID STORE 1:3,5,7:9,11:14,26:29,31,34,41:43,50 +X-GM-LABELS (\\Important \"Custom Label\" NIL)\r\n", "gmail.add-labels.txt")); + commands.Add (new ImapReplayCommand ("A00000085 UID STORE 1:3,5,7:9,11:14,26:29,31,34,41:43,50 (UNCHANGEDSINCE 5) X-GM-LABELS (\\Important \"Custom Label\" NIL)\r\n", "gmail.set-labels.txt")); + commands.Add (new ImapReplayCommand ("A00000086 UID STORE 1:3,5,7:9,11:14,26:29,31,34,41:43,50 (UNCHANGEDSINCE 5) -X-GM-LABELS.SILENT (\\Important \"Custom Label\" NIL)\r\n", ImapReplayCommandResponse.OK)); + commands.Add (new ImapReplayCommand ("A00000087 UID STORE 1:3,5,7:9,11:14,26:29,31,34,41:43,50 (UNCHANGEDSINCE 5) +X-GM-LABELS (\\Important \"Custom Label\" NIL)\r\n", "gmail.add-labels.txt")); + commands.Add (new ImapReplayCommand ("A00000088 STORE 1:3,5,7:9,11:14,26:29,31,34,41:43,50 X-GM-LABELS (\\Important \"Custom Label\" NIL)\r\n", "gmail.set-labels.txt")); + commands.Add (new ImapReplayCommand ("A00000089 STORE 1:3,5,7:9,11:14,26:29,31,34,41:43,50 -X-GM-LABELS.SILENT (\\Important \"Custom Label\" NIL)\r\n", ImapReplayCommandResponse.OK)); + commands.Add (new ImapReplayCommand ("A00000090 STORE 1:3,5,7:9,11:14,26:29,31,34,41:43,50 +X-GM-LABELS (\\Important \"Custom Label\" NIL)\r\n", "gmail.add-labels.txt")); + commands.Add (new ImapReplayCommand ("A00000091 STORE 1:3,5,7:9,11:14,26:29,31,34,41:43,50 (UNCHANGEDSINCE 5) X-GM-LABELS (\\Important \"Custom Label\" NIL)\r\n", "gmail.set-labels.txt")); + commands.Add (new ImapReplayCommand ("A00000092 STORE 1:3,5,7:9,11:14,26:29,31,34,41:43,50 (UNCHANGEDSINCE 5) -X-GM-LABELS.SILENT (\\Important \"Custom Label\" NIL)\r\n", ImapReplayCommandResponse.OK)); + commands.Add (new ImapReplayCommand ("A00000093 STORE 1:3,5,7:9,11:14,26:29,31,34,41:43,50 (UNCHANGEDSINCE 5) +X-GM-LABELS (\\Important \"Custom Label\" NIL)\r\n", "gmail.add-labels.txt")); + commands.Add (new ImapReplayCommand ("A00000094 UID STORE 1:3,5,7:9,11:14,26:29,31,34,41:43,50 FLAGS (\\Answered \\Seen)\r\n", "gmail.set-flags.txt")); + commands.Add (new ImapReplayCommand ("A00000095 UID STORE 1:3,5,7:9,11:14,26:29,31,34,41:43,50 -FLAGS.SILENT (\\Answered)\r\n", ImapReplayCommandResponse.OK)); + commands.Add (new ImapReplayCommand ("A00000096 UID STORE 1:3,5,7:9,11:14,26:29,31,34,41:43,50 +FLAGS.SILENT (\\Deleted)\r\n", "gmail.add-flags.txt")); + commands.Add (new ImapReplayCommand ("A00000097 CHECK\r\n", ImapReplayCommandResponse.OK)); + commands.Add (new ImapReplayCommand ("A00000098 UNSELECT\r\n", ImapReplayCommandResponse.OK)); + commands.Add (new ImapReplayCommand ("A00000099 SUBSCRIBE UnitTests\r\n", ImapReplayCommandResponse.OK)); + commands.Add (new ImapReplayCommand ("A00000100 LSUB \"\" \"%\"\r\n", "gmail.lsub-personal.txt")); + commands.Add (new ImapReplayCommand ("A00000101 UNSUBSCRIBE UnitTests\r\n", ImapReplayCommandResponse.OK)); + commands.Add (new ImapReplayCommand ("A00000102 CREATE UnitTests/Dummy\r\n", ImapReplayCommandResponse.OK)); + commands.Add (new ImapReplayCommand ("A00000103 LIST \"\" UnitTests/Dummy\r\n", "gmail.list-unittests-dummy.txt")); + commands.Add (new ImapReplayCommand ("A00000104 RENAME UnitTests RenamedUnitTests\r\n", ImapReplayCommandResponse.OK)); + commands.Add (new ImapReplayCommand ("A00000105 DELETE RenamedUnitTests\r\n", ImapReplayCommandResponse.OK)); + commands.Add (new ImapReplayCommand ("A00000106 LOGOUT\r\n", "gmail.logout.txt")); + + return commands; + } + + [Test] + public void TestGMail () + { + var commands = CreateGMailCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + + Assert.That (client.Capabilities, Is.EqualTo (GMailInitialCapabilities)); + Assert.That (client.AuthenticationMechanisms, Has.Count.EqualTo (5)); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH"), "Expected SASL XOAUTH auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH2"), "Expected SASL XOAUTH2 auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("OAUTHBEARER"), "Expected SASL OAUTHBEARER auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN"), "Expected SASL PLAIN auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN-CLIENTTOKEN"), "Expected SASL PLAIN-CLIENTTOKEN auth mechanism"); + + // Note: Do not try XOAUTH2 + client.AuthenticationMechanisms.Remove ("XOAUTH2"); + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities, Is.EqualTo (GMailAuthenticatedCapabilities)); + Assert.That (client.AppendLimit.HasValue, Is.True, "Expected AppendLimit to have a value"); + Assert.That (client.AppendLimit.Value, Is.EqualTo (35651584), "Expected AppendLimit value to match"); + + Assert.Throws (() => client.EnableQuickResync ()); + Assert.Throws (() => client.Notify (true, new List { + new ImapEventGroup (ImapMailboxFilter.Inboxes, ImapEvent.FlagChange, new ImapEvent.MessageNew (), ImapEvent.MessageExpunge) + })); + Assert.Throws (() => client.DisableNotify ()); + + var inbox = client.Inbox; + Assert.That (inbox, Is.Not.Null, "Expected non-null Inbox folder."); + Assert.That (inbox.Attributes, Is.EqualTo (FolderAttributes.Inbox | FolderAttributes.HasNoChildren | FolderAttributes.Subscribed), "Expected Inbox attributes to be \\HasNoChildren."); + + foreach (var special in Enum.GetValues (typeof (SpecialFolder)).OfType ()) { + var folder = client.GetFolder (special); + + if (special != SpecialFolder.Archive) { + var expected = GetSpecialFolderAttribute (special) | FolderAttributes.HasNoChildren; + + Assert.That (folder, Is.Not.Null, $"Expected non-null {special} folder."); + Assert.That (folder.Attributes, Is.EqualTo (expected), $"Expected {special} attributes to be \\HasNoChildren."); + } else { + Assert.That (folder, Is.Null, $"Expected null {special} folder."); + } + } + + // disable LIST-EXTENDED + client.Capabilities &= ~ImapCapabilities.ListExtended; + + var personal = client.GetFolder (client.PersonalNamespaces[0]); + var folders = personal.GetSubfolders (); + Assert.That (folders[0], Is.EqualTo (client.Inbox), "Expected the first folder to be the Inbox."); + Assert.That (folders[1].FullName, Is.EqualTo ("[Gmail]"), "Expected the second folder to be [Gmail]."); + Assert.That (folders[1].Attributes, Is.EqualTo (FolderAttributes.NoSelect | FolderAttributes.HasChildren), "Expected [Gmail] folder to be \\Noselect \\HasChildren."); + + var created = personal.Create ("UnitTests", true); + Assert.That (created, Is.Not.Null, "Expected a non-null created folder."); + Assert.That (created.Attributes, Is.EqualTo (FolderAttributes.HasNoChildren)); + + Assert.That (created.ParentFolder, Is.Not.Null, "The ParentFolder property should not be null."); + + const MessageFlags ExpectedPermanentFlags = MessageFlags.Answered | MessageFlags.Flagged | MessageFlags.Draft | MessageFlags.Deleted | MessageFlags.Seen | MessageFlags.UserDefined; + const MessageFlags ExpectedAcceptedFlags = MessageFlags.Answered | MessageFlags.Flagged | MessageFlags.Draft | MessageFlags.Deleted | MessageFlags.Seen; + var access = created.Open (FolderAccess.ReadWrite); + Assert.That (access, Is.EqualTo (FolderAccess.ReadWrite), "The UnitTests folder was not opened with the expected access mode."); + Assert.That (created.PermanentFlags, Is.EqualTo (ExpectedPermanentFlags), "The PermanentFlags do not match the expected value."); + Assert.That (created.AcceptedFlags, Is.EqualTo (ExpectedAcceptedFlags), "The AcceptedFlags do not match the expected value."); + + for (int i = 0; i < 50; i++) { + using (var stream = GetResourceStream (string.Format ("common.message.{0}.msg", i))) { + using (var message = MimeMessage.Load (stream)) { + var uid = created.Append (message, MessageFlags.Seen); + Assert.That (uid.HasValue, Is.True, "Expected a UID to be returned from folder.Append()."); + Assert.That (uid.Value.Id, Is.EqualTo ((uint) (i + 1)), "The UID returned from the APPEND command does not match the expected UID."); + } + } + } + + var query = SearchQuery.GMailMessageId (1).Or (SearchQuery.GMailThreadId (5).Or (SearchQuery.HasGMailLabel ("Custom Label").Or (SearchQuery.GMailRawSearch ("has:attachment in:unread")))); + var matches = created.Search (query); + Assert.That (matches, Has.Count.EqualTo (21)); + + const MessageSummaryItems items = MessageSummaryItems.Full | MessageSummaryItems.UniqueId | MessageSummaryItems.GMailLabels | MessageSummaryItems.GMailMessageId | MessageSummaryItems.GMailThreadId; + var summaries = created.Fetch (matches, items); + var indexes = new List (); + + foreach (var summary in summaries) { + Assert.That (summary.GMailMessageId.Value, Is.EqualTo (1592225494819146100 + summary.UniqueId.Id), "GMailMessageId"); + Assert.That (summary.GMailThreadId.Value, Is.EqualTo (1592225494819146100 + summary.UniqueId.Id), "GMailThreadId"); + Assert.That (summary.GMailLabels, Has.Count.EqualTo (2), "GMailLabels.Count"); + Assert.That (summary.GMailLabels[0], Is.EqualTo ("Test Messages")); + Assert.That (summary.GMailLabels[1], Is.EqualTo ("\\Important")); + Assert.That (summary.UniqueId.IsValid, Is.True, "UniqueId.IsValid"); + + created.GetMessage (summary.UniqueId); + indexes.Add (summary.Index); + } + + var labels = new [] { "\\Important", "Custom Label", null }; + created.SetLabels (matches, labels, false); + created.RemoveLabels (matches, labels, true); + created.AddLabels (matches, labels, false); + + created.SetLabels (matches, 5, labels, false); + created.RemoveLabels (matches, 5, labels, true); + created.AddLabels (matches, 5, labels, false); + + created.SetLabels (indexes, labels, false); + created.RemoveLabels (indexes, labels, true); + created.AddLabels (indexes, labels, false); + + created.SetLabels (indexes, 5, labels, false); + created.RemoveLabels (indexes, 5, labels, true); + created.AddLabels (indexes, 5, labels, false); + + // Verify that Adding and/or removing an empty set of labels is a no-op + labels = Array.Empty (); + + created.RemoveLabels (matches, labels, true); + created.AddLabels (matches, labels, false); + + created.RemoveLabels (matches, 5, labels, true); + created.AddLabels (matches, 5, labels, false); + + created.RemoveLabels (indexes, labels, true); + created.AddLabels (indexes, labels, false); + + created.RemoveLabels (indexes, 5, labels, true); + created.AddLabels (indexes, 5, labels, false); + + created.SetFlags (matches, MessageFlags.Seen | MessageFlags.Answered, false); + created.RemoveFlags (matches, MessageFlags.Answered, true); + created.AddFlags (matches, MessageFlags.Deleted, true); + + // Verify that Adding and/or removing an empty set of flags is a no-op + created.RemoveFlags (matches, MessageFlags.None, true); + created.AddFlags (matches, MessageFlags.None, true); + + created.RemoveFlags (matches, 5, MessageFlags.None, true); + created.AddFlags (matches, 5, MessageFlags.None, true); + + created.RemoveFlags (indexes, MessageFlags.None, true); + created.AddFlags (indexes, MessageFlags.None, true); + + created.RemoveFlags (indexes, 5, MessageFlags.None, true); + created.AddFlags (indexes, 5, MessageFlags.None, true); + + created.Check (); + + created.Close (); + Assert.That (created.IsOpen, Is.False, "Expected the UnitTests folder to be closed."); + + created.Subscribe (); + Assert.That (created.IsSubscribed, Is.True, "Expected IsSubscribed to be true after subscribing to the folder."); + + var subscribed = personal.GetSubfolders (true); + Assert.That (subscribed.Contains (created), Is.True, "Expected the list of subscribed folders to contain the UnitTests folder."); + + created.Unsubscribe (); + Assert.That (created.IsSubscribed, Is.False, "Expected IsSubscribed to be false after unsubscribing from the folder."); + + var dummy = created.Create ("Dummy", true); + bool dummyRenamed = false; + bool renamed = false; + bool deleted = false; + + dummy.Renamed += (sender, e) => { dummyRenamed = true; }; + created.Renamed += (sender, e) => { renamed = true; }; + + created.Rename (created.ParentFolder, "RenamedUnitTests"); + Assert.That (created.Name, Is.EqualTo ("RenamedUnitTests")); + Assert.That (created.FullName, Is.EqualTo ("RenamedUnitTests")); + Assert.That (renamed, Is.True, "Expected the Rename event to be emitted for the UnitTests folder."); + + Assert.That (dummy.FullName, Is.EqualTo ("RenamedUnitTests/Dummy")); + Assert.That (dummyRenamed, Is.True, "Expected the Rename event to be emitted for the UnitTests/Dummy folder."); + + created.Deleted += (sender, e) => { deleted = true; }; + + created.Delete (); + Assert.That (deleted, Is.True, "Expected the Deleted event to be emitted for the UnitTests folder."); + Assert.That (created.Exists, Is.False, "Expected Exists to be false after deleting the folder."); + + client.Disconnect (true); + } + } + + [Test] + public async Task TestGMailAsync () + { + var commands = CreateGMailCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + + Assert.That (client.Capabilities, Is.EqualTo (GMailInitialCapabilities)); + Assert.That (client.AuthenticationMechanisms, Has.Count.EqualTo (5)); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH"), "Expected SASL XOAUTH auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH2"), "Expected SASL XOAUTH2 auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("OAUTHBEARER"), "Expected SASL OAUTHBEARER auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN"), "Expected SASL PLAIN auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN-CLIENTTOKEN"), "Expected SASL PLAIN-CLIENTTOKEN auth mechanism"); + + // Note: Do not try XOAUTH2 + client.AuthenticationMechanisms.Remove ("XOAUTH2"); + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities, Is.EqualTo (GMailAuthenticatedCapabilities)); + Assert.That (client.AppendLimit.HasValue, Is.True, "Expected AppendLimit to have a value"); + Assert.That (client.AppendLimit.Value, Is.EqualTo (35651584), "Expected AppendLimit value to match"); + + Assert.ThrowsAsync (async () => await client.EnableQuickResyncAsync ()); + Assert.ThrowsAsync (async () => await client.NotifyAsync (true, new List { + new ImapEventGroup (ImapMailboxFilter.Inboxes, ImapEvent.FlagChange, new ImapEvent.MessageNew (), ImapEvent.MessageExpunge) + })); + Assert.ThrowsAsync (async () => await client.DisableNotifyAsync ()); + + var inbox = client.Inbox; + Assert.That (inbox, Is.Not.Null, "Expected non-null Inbox folder."); + Assert.That (inbox.Attributes, Is.EqualTo (FolderAttributes.Inbox | FolderAttributes.HasNoChildren | FolderAttributes.Subscribed), "Expected Inbox attributes to be \\HasNoChildren."); + + foreach (var special in Enum.GetValues (typeof (SpecialFolder)).OfType ()) { + var folder = client.GetFolder (special); + + if (special != SpecialFolder.Archive) { + var expected = GetSpecialFolderAttribute (special) | FolderAttributes.HasNoChildren; + + Assert.That (folder, Is.Not.Null, $"Expected non-null {special} folder."); + Assert.That (folder.Attributes, Is.EqualTo (expected), $"Expected {special} attributes to be \\HasNoChildren."); + } else { + Assert.That (folder, Is.Null, $"Expected null {special} folder."); + } + } + + // disable LIST-EXTENDED + client.Capabilities &= ~ImapCapabilities.ListExtended; + + var personal = client.GetFolder (client.PersonalNamespaces[0]); + var folders = await personal.GetSubfoldersAsync (); + Assert.That (folders[0], Is.EqualTo (client.Inbox), "Expected the first folder to be the Inbox."); + Assert.That (folders[1].FullName, Is.EqualTo ("[Gmail]"), "Expected the second folder to be [Gmail]."); + Assert.That (folders[1].Attributes, Is.EqualTo (FolderAttributes.NoSelect | FolderAttributes.HasChildren), "Expected [Gmail] folder to be \\Noselect \\HasChildren."); + + var created = await personal.CreateAsync ("UnitTests", true); + Assert.That (created, Is.Not.Null, "Expected a non-null created folder."); + Assert.That (created.Attributes, Is.EqualTo (FolderAttributes.HasNoChildren)); + + Assert.That (created.ParentFolder, Is.Not.Null, "The ParentFolder property should not be null."); + + const MessageFlags ExpectedPermanentFlags = MessageFlags.Answered | MessageFlags.Flagged | MessageFlags.Draft | MessageFlags.Deleted | MessageFlags.Seen | MessageFlags.UserDefined; + const MessageFlags ExpectedAcceptedFlags = MessageFlags.Answered | MessageFlags.Flagged | MessageFlags.Draft | MessageFlags.Deleted | MessageFlags.Seen; + var access = await created.OpenAsync (FolderAccess.ReadWrite); + Assert.That (access, Is.EqualTo (FolderAccess.ReadWrite), "The UnitTests folder was not opened with the expected access mode."); + Assert.That (created.PermanentFlags, Is.EqualTo (ExpectedPermanentFlags), "The PermanentFlags do not match the expected value."); + Assert.That (created.AcceptedFlags, Is.EqualTo (ExpectedAcceptedFlags), "The AcceptedFlags do not match the expected value."); + + for (int i = 0; i < 50; i++) { + using (var stream = GetResourceStream (string.Format ("common.message.{0}.msg", i))) { + using (var message = MimeMessage.Load (stream)) { + var uid = await created.AppendAsync (message, MessageFlags.Seen); + Assert.That (uid.HasValue, Is.True, "Expected a UID to be returned from folder.Append()."); + Assert.That (uid.Value.Id, Is.EqualTo ((uint) (i + 1)), "The UID returned from the APPEND command does not match the expected UID."); + } + } + } + + var query = SearchQuery.GMailMessageId (1).Or (SearchQuery.GMailThreadId (5).Or (SearchQuery.HasGMailLabel ("Custom Label").Or (SearchQuery.GMailRawSearch ("has:attachment in:unread")))); + var matches = await created.SearchAsync (query); + Assert.That (matches, Has.Count.EqualTo (21)); + + const MessageSummaryItems items = MessageSummaryItems.Full | MessageSummaryItems.UniqueId | MessageSummaryItems.GMailLabels | MessageSummaryItems.GMailMessageId | MessageSummaryItems.GMailThreadId; + var summaries = await created.FetchAsync (matches, items); + var indexes = new List (); + + foreach (var summary in summaries) { + Assert.That (summary.GMailMessageId.Value, Is.EqualTo (1592225494819146100 + summary.UniqueId.Id), "GMailMessageId"); + Assert.That (summary.GMailThreadId.Value, Is.EqualTo (1592225494819146100 + summary.UniqueId.Id), "GMailThreadId"); + Assert.That (summary.GMailLabels, Has.Count.EqualTo (2), "GMailLabels.Count"); + Assert.That (summary.GMailLabels[0], Is.EqualTo ("Test Messages")); + Assert.That (summary.GMailLabels[1], Is.EqualTo ("\\Important")); + Assert.That (summary.UniqueId.IsValid, Is.True, "UniqueId.IsValid"); + + await created.GetMessageAsync (summary.UniqueId); + indexes.Add (summary.Index); + } + + var labels = new [] { "\\Important", "Custom Label", null }; + await created.SetLabelsAsync (matches, labels, false); + await created.RemoveLabelsAsync (matches, labels, true); + await created.AddLabelsAsync (matches, labels, false); + + await created.SetLabelsAsync (matches, 5, labels, false); + await created.RemoveLabelsAsync (matches, 5, labels, true); + await created.AddLabelsAsync (matches, 5, labels, false); + + await created.SetLabelsAsync (indexes, labels, false); + await created.RemoveLabelsAsync (indexes, labels, true); + await created.AddLabelsAsync (indexes, labels, false); + + await created.SetLabelsAsync (indexes, 5, labels, false); + await created.RemoveLabelsAsync (indexes, 5, labels, true); + await created.AddLabelsAsync (indexes, 5, labels, false); + + // Verify that Adding and/or removing an empty set of labels is a no-op + labels = Array.Empty (); + + await created.RemoveLabelsAsync (matches, labels, true); + await created.AddLabelsAsync (matches, labels, false); + + await created.RemoveLabelsAsync (matches, 5, labels, true); + await created.AddLabelsAsync (matches, 5, labels, false); + + await created.RemoveLabelsAsync (indexes, labels, true); + await created.AddLabelsAsync (indexes, labels, false); + + await created.RemoveLabelsAsync (indexes, 5, labels, true); + await created.AddLabelsAsync (indexes, 5, labels, false); + + await created.SetFlagsAsync (matches, MessageFlags.Seen | MessageFlags.Answered, false); + await created.RemoveFlagsAsync (matches, MessageFlags.Answered, true); + await created.AddFlagsAsync (matches, MessageFlags.Deleted, true); + + // Verify that Adding and/or removing an empty set of flags is a no-op + await created.RemoveFlagsAsync (matches, MessageFlags.None, true); + await created.AddFlagsAsync (matches, MessageFlags.None, true); + + await created.RemoveFlagsAsync (matches, 5, MessageFlags.None, true); + await created.AddFlagsAsync (matches, 5, MessageFlags.None, true); + + await created.RemoveFlagsAsync (indexes, MessageFlags.None, true); + await created.AddFlagsAsync (indexes, MessageFlags.None, true); + + await created.RemoveFlagsAsync (indexes, 5, MessageFlags.None, true); + await created.AddFlagsAsync (indexes, 5, MessageFlags.None, true); + + await created.CheckAsync (); + + await created.CloseAsync (); + Assert.That (created.IsOpen, Is.False, "Expected the UnitTests folder to be closed."); + + await created.SubscribeAsync (); + Assert.That (created.IsSubscribed, Is.True, "Expected IsSubscribed to be true after subscribing to the folder."); + + var subscribed = await personal.GetSubfoldersAsync (true); + Assert.That (subscribed.Contains (created), Is.True, "Expected the list of subscribed folders to contain the UnitTests folder."); + await created.UnsubscribeAsync (); + Assert.That (created.IsSubscribed, Is.False, "Expected IsSubscribed to be false after unsubscribing from the folder."); + + var dummy = await created.CreateAsync ("Dummy", true); + bool dummyRenamed = false; + bool renamed = false; + bool deleted = false; + + dummy.Renamed += (sender, e) => { dummyRenamed = true; }; + created.Renamed += (sender, e) => { renamed = true; }; + + await created.RenameAsync (created.ParentFolder, "RenamedUnitTests"); + Assert.That (created.Name, Is.EqualTo ("RenamedUnitTests")); + Assert.That (created.FullName, Is.EqualTo ("RenamedUnitTests")); + Assert.That (renamed, Is.True, "Expected the Rename event to be emitted for the UnitTests folder."); + + Assert.That (dummy.FullName, Is.EqualTo ("RenamedUnitTests/Dummy")); + Assert.That (dummyRenamed, Is.True, "Expected the Rename event to be emitted for the UnitTests/Dummy folder."); + + created.Deleted += (sender, e) => { deleted = true; }; + + await created.DeleteAsync (); + Assert.That (deleted, Is.True, "Expected the Deleted event to be emitted for the UnitTests folder."); + Assert.That (created.Exists, Is.False, "Expected Exists to be false after deleting the folder."); + + await client.DisconnectAsync (true); + } + } + + static List CreateGetFolderCommands () + { + return new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt"), + new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "gmail.authenticate.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-inbox.txt"), + new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt"), + new ImapReplayCommand ("A00000005 LIST \"\" Level1/Level2/Level3 RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-level3.txt"), + new ImapReplayCommand ("A00000006 LIST \"\" Level1/Level2 RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-level2.txt"), + new ImapReplayCommand ("A00000007 LIST \"\" Level1 RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-level1.txt"), + new ImapReplayCommand ("A00000008 LOGOUT\r\n", "gmail.logout.txt") + }; + } + + [Test] + public void TestGetFolder () + { + var commands = CreateGetFolderCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { try { - await client.EnableQuickResyncAsync (); + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); } catch (Exception ex) { - Assert.Fail ("Did not expect an exception when enabling QRESYNC: {0}", ex); + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); } - // take advantage of LIST-STATUS to get top-level personal folders... - var statusItems = StatusItems.Count | StatusItems.HighestModSeq | StatusItems.Recent | StatusItems.UidNext | StatusItems.UidValidity | StatusItems.Unread; + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); - var folders = (await personal.GetSubfoldersAsync (statusItems, false)).ToArray (); - Assert.AreEqual (7, folders.Length, "Expected 7 folders"); + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } - var expectedFolderNames = new [] { "Archives", "Drafts", "Junk", "Sent Messages", "Trash", "INBOX", "NIL" }; - var expectedUidValidities = new [] { 1436832059, 1436832060, 1436832061, 1436832062, 1436832063, 1436832057, 1436832057 }; - var expectedHighestModSeq = new [] { 1, 1, 1, 1, 1, 15, 1 }; - var expectedMessages = new [] { 0, 0, 0, 0, 0, 4, 0 }; - var expectedUidNext = new [] { 1, 1, 1, 1, 1, 5, 1 }; - var expectedRecent = new [] { 0, 0, 0, 0, 0, 0, 0 }; - var expectedUnseen = new [] { 0, 0, 0, 0, 0, 0, 0 }; + var level3 = client.GetFolder ("Level1/Level2/Level3"); + Assert.That (level3.FullName, Is.EqualTo ("Level1/Level2/Level3")); + Assert.That (level3.Name, Is.EqualTo ("Level3")); + Assert.That (level3.DirectorySeparator, Is.EqualTo ('/')); + Assert.That (level3.Attributes, Is.EqualTo (FolderAttributes.HasNoChildren)); + + var level2 = level3.ParentFolder; + Assert.That (level2.FullName, Is.EqualTo ("Level1/Level2")); + Assert.That (level2.Name, Is.EqualTo ("Level2")); + Assert.That (level2.DirectorySeparator, Is.EqualTo ('/')); + Assert.That (level2.Attributes, Is.EqualTo (FolderAttributes.HasChildren)); + + var level1 = level2.ParentFolder; + Assert.That (level1.FullName, Is.EqualTo ("Level1")); + Assert.That (level1.Name, Is.EqualTo ("Level1")); + Assert.That (level1.DirectorySeparator, Is.EqualTo ('/')); + Assert.That (level1.Attributes, Is.EqualTo (FolderAttributes.HasChildren)); + + var personal = level1.ParentFolder; + Assert.That (personal.FullName, Is.EqualTo (string.Empty)); + Assert.That (personal.Name, Is.EqualTo (string.Empty)); + Assert.That (personal.IsNamespace, Is.True, "IsNamespace"); + + client.Disconnect (true); + } + } - for (int i = 0; i < folders.Length; i++) { - Assert.AreEqual (expectedFolderNames[i], folders[i].FullName, "FullName did not match"); - Assert.AreEqual (expectedFolderNames[i], folders[i].Name, "Name did not match"); - Assert.AreEqual (expectedUidValidities[i], folders[i].UidValidity, "UidValidity did not match"); - Assert.AreEqual (expectedHighestModSeq[i], folders[i].HighestModSeq, "HighestModSeq did not match"); - Assert.AreEqual (expectedMessages[i], folders[i].Count, "Count did not match"); - Assert.AreEqual (expectedRecent[i], folders[i].Recent, "Recent did not match"); - Assert.AreEqual (expectedUnseen[i], folders[i].Unread, "Unread did not match"); + [Test] + public async Task TestGetFolderAsync () + { + var commands = CreateGetFolderCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); } - var unitTests = await personal.CreateAsync ("UnitTests", false); - Assert.AreEqual (FolderAttributes.HasNoChildren, unitTests.Attributes, "Unexpected UnitTests folder attributes"); + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); - var folder = await unitTests.CreateAsync ("Messages", true); - Assert.AreEqual (FolderAttributes.HasNoChildren, folder.Attributes, "Unexpected UnitTests.Messages folder attributes"); - //Assert.AreEqual (FolderAttributes.HasChildren, unitTests.Attributes, "Expected UnitTests Attributes to be updated"); + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } - // Use MULTIAPPEND to append some test messages - var appended = await folder.AppendAsync (messages, flags, internalDates); - Assert.AreEqual (8, appended.Count, "Unexpected number of messages appended"); + var level3 = await client.GetFolderAsync ("Level1/Level2/Level3"); + Assert.That (level3.FullName, Is.EqualTo ("Level1/Level2/Level3")); + Assert.That (level3.Name, Is.EqualTo ("Level3")); + Assert.That (level3.DirectorySeparator, Is.EqualTo ('/')); + Assert.That (level3.Attributes, Is.EqualTo (FolderAttributes.HasNoChildren)); + + var level2 = level3.ParentFolder; + Assert.That (level2.FullName, Is.EqualTo ("Level1/Level2")); + Assert.That (level2.Name, Is.EqualTo ("Level2")); + Assert.That (level2.DirectorySeparator, Is.EqualTo ('/')); + Assert.That (level2.Attributes, Is.EqualTo (FolderAttributes.HasChildren)); + + var level1 = level2.ParentFolder; + Assert.That (level1.FullName, Is.EqualTo ("Level1")); + Assert.That (level1.Name, Is.EqualTo ("Level1")); + Assert.That (level1.DirectorySeparator, Is.EqualTo ('/')); + Assert.That (level1.Attributes, Is.EqualTo (FolderAttributes.HasChildren)); + + var personal = level1.ParentFolder; + Assert.That (personal.FullName, Is.EqualTo (string.Empty)); + Assert.That (personal.Name, Is.EqualTo (string.Empty)); + Assert.That (personal.IsNamespace, Is.True, "IsNamespace"); - // SELECT the folder so that we can test some stuff - var access = await folder.OpenAsync (FolderAccess.ReadWrite); - Assert.AreEqual (expectedPermanentFlags, folder.PermanentFlags, "UnitTests.Messages PERMANENTFLAGS"); - Assert.AreEqual (expectedFlags, folder.AcceptedFlags, "UnitTests.Messages FLAGS"); - Assert.AreEqual (8, folder.Count, "UnitTests.Messages EXISTS"); - Assert.AreEqual (8, folder.Recent, "UnitTests.Messages RECENT"); - Assert.AreEqual (0, folder.FirstUnread, "UnitTests.Messages UNSEEN"); - Assert.AreEqual (1436832084U, folder.UidValidity, "UnitTests.Messages UIDVALIDITY"); - Assert.AreEqual (9, folder.UidNext.Value.Id, "UnitTests.Messages UIDNEXT"); - Assert.AreEqual (2UL, folder.HighestModSeq, "UnitTests.Messages HIGHESTMODSEQ"); - Assert.AreEqual (FolderAccess.ReadWrite, access, "Expected UnitTests.Messages to be opened in READ-WRITE mode"); + await client.DisconnectAsync (true); + } + } - // Keep track of various folder events - var flagsChanged = new List (); - var modSeqChanged = new List (); - var vanished = new List (); - bool recentChanged = false; + static List CreateIdentifyCommands () + { + return new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt"), + new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "gmail.authenticate.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-inbox.txt"), + new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt"), + new ImapReplayCommand ("A00000005 ID NIL\r\n", "common.id.txt"), + new ImapReplayCommand ("A00000006 ID (\"name\" \"MailKit\" \"version\" \"1.0\" \"vendor\" \"Xamarin Inc.\" \"address\" {35+}\r\n1 Memorial Dr.\r\nCambridge, MA 02142)\r\n", "common.id.txt"), + new ImapReplayCommand ("A00000007 ID (\"name\" \"MailKit\" \"version\" \"1.0\" \"vendor\" \"Xamarin Inc.\" \"address\" NIL)\r\n", "common.id.txt"), + }; + } - folder.MessageFlagsChanged += (sender, e) => { - flagsChanged.Add (e); + [Test] + public void TestIdentify () + { + var commands = CreateIdentifyCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + ImapImplementation implementation; + + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + Assert.That (client.IsSecure, Is.False, "IsSecure should be false."); + + Assert.That (client.Capabilities, Is.EqualTo (GMailInitialCapabilities)); + Assert.That (client.AuthenticationMechanisms, Has.Count.EqualTo (5)); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH"), "Expected SASL XOAUTH auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH2"), "Expected SASL XOAUTH2 auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("OAUTHBEARER"), "Expected SASL OAUTHBEARER auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN"), "Expected SASL PLAIN auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN-CLIENTTOKEN"), "Expected SASL PLAIN-CLIENTTOKEN auth mechanism"); + + try { + client.Authenticate (new NetworkCredential ("username", "password")); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities, Is.EqualTo (GMailAuthenticatedCapabilities)); + + implementation = client.Identify (null); + Assert.That (implementation, Is.Not.Null, "Expected a non-null ID response."); + Assert.That (implementation.Name, Is.EqualTo ("GImap")); + Assert.That (implementation.Vendor, Is.EqualTo ("Google, Inc.")); + Assert.That (implementation.SupportUrl, Is.EqualTo ("http://support.google.com/mail")); + Assert.That (implementation.Version, Is.EqualTo ("gmail_imap_150623.03_p1")); + Assert.That (implementation.Properties["remote-host"], Is.EqualTo ("127.0.0.1")); + + implementation = new ImapImplementation { + Name = "MailKit", + Version = "1.0", + Vendor = "Xamarin Inc.", + Address = "1 Memorial Dr.\r\nCambridge, MA 02142" }; - folder.ModSeqChanged += (sender, e) => { - modSeqChanged.Add (e); + implementation = client.Identify (implementation); + Assert.That (implementation, Is.Not.Null, "Expected a non-null ID response."); + Assert.That (implementation.Name, Is.EqualTo ("GImap")); + Assert.That (implementation.Vendor, Is.EqualTo ("Google, Inc.")); + Assert.That (implementation.SupportUrl, Is.EqualTo ("http://support.google.com/mail")); + Assert.That (implementation.Version, Is.EqualTo ("gmail_imap_150623.03_p1")); + Assert.That (implementation.Properties["remote-host"], Is.EqualTo ("127.0.0.1")); + + implementation = new ImapImplementation { + Name = "MailKit", + Version = "1.0", + Vendor = "Xamarin Inc.", + Address = null }; - folder.MessagesVanished += (sender, e) => { - vanished.Add (e); + implementation = client.Identify (implementation); + Assert.That (implementation, Is.Not.Null, "Expected a non-null ID response."); + Assert.That (implementation.Name, Is.EqualTo ("GImap")); + Assert.That (implementation.Vendor, Is.EqualTo ("Google, Inc.")); + Assert.That (implementation.SupportUrl, Is.EqualTo ("http://support.google.com/mail")); + Assert.That (implementation.Version, Is.EqualTo ("gmail_imap_150623.03_p1")); + Assert.That (implementation.Properties["remote-host"], Is.EqualTo ("127.0.0.1")); + + // disable ID support + client.Capabilities &= ~ImapCapabilities.Id; + Assert.Throws (() => client.Identify (null)); + + client.Disconnect (false); + } + } + + [Test] + public async Task TestIdentifyAsync () + { + var commands = CreateIdentifyCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + ImapImplementation implementation; + + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + Assert.That (client.IsSecure, Is.False, "IsSecure should be false."); + + Assert.That (client.Capabilities, Is.EqualTo (GMailInitialCapabilities)); + Assert.That (client.AuthenticationMechanisms, Has.Count.EqualTo (5)); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH"), "Expected SASL XOAUTH auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH2"), "Expected SASL XOAUTH2 auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("OAUTHBEARER"), "Expected SASL OAUTHBEARER auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN"), "Expected SASL PLAIN auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN-CLIENTTOKEN"), "Expected SASL PLAIN-CLIENTTOKEN auth mechanism"); + + try { + await client.AuthenticateAsync (new NetworkCredential ("username", "password")); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities, Is.EqualTo (GMailAuthenticatedCapabilities)); + + implementation = await client.IdentifyAsync (null); + Assert.That (implementation, Is.Not.Null, "Expected a non-null ID response."); + Assert.That (implementation.Name, Is.EqualTo ("GImap")); + Assert.That (implementation.Vendor, Is.EqualTo ("Google, Inc.")); + Assert.That (implementation.SupportUrl, Is.EqualTo ("http://support.google.com/mail")); + Assert.That (implementation.Version, Is.EqualTo ("gmail_imap_150623.03_p1")); + Assert.That (implementation.Properties["remote-host"], Is.EqualTo ("127.0.0.1")); + + implementation = new ImapImplementation { + Name = "MailKit", + Version = "1.0", + Vendor = "Xamarin Inc.", + Address = "1 Memorial Dr.\r\nCambridge, MA 02142" }; - folder.RecentChanged += (sender, e) => { - recentChanged = true; + implementation = await client.IdentifyAsync (implementation); + Assert.That (implementation, Is.Not.Null, "Expected a non-null ID response."); + Assert.That (implementation.Name, Is.EqualTo ("GImap")); + Assert.That (implementation.Vendor, Is.EqualTo ("Google, Inc.")); + Assert.That (implementation.SupportUrl, Is.EqualTo ("http://support.google.com/mail")); + Assert.That (implementation.Version, Is.EqualTo ("gmail_imap_150623.03_p1")); + Assert.That (implementation.Properties["remote-host"], Is.EqualTo ("127.0.0.1")); + + implementation = new ImapImplementation { + Name = "MailKit", + Version = "1.0", + Vendor = "Xamarin Inc.", + Address = null }; - // Keep track of UIDVALIDITY and HIGHESTMODSEQ values for our QRESYNC test later - var highestModSeq = folder.HighestModSeq; - var uidValidity = folder.UidValidity; + implementation = await client.IdentifyAsync (implementation); + Assert.That (implementation, Is.Not.Null, "Expected a non-null ID response."); + Assert.That (implementation.Name, Is.EqualTo ("GImap")); + Assert.That (implementation.Vendor, Is.EqualTo ("Google, Inc.")); + Assert.That (implementation.SupportUrl, Is.EqualTo ("http://support.google.com/mail")); + Assert.That (implementation.Version, Is.EqualTo ("gmail_imap_150623.03_p1")); + Assert.That (implementation.Properties["remote-host"], Is.EqualTo ("127.0.0.1")); - // Make some FLAGS changes to our messages so we can test QRESYNC - await folder.AddFlagsAsync (appended, MessageFlags.Seen, true); - Assert.AreEqual (0, flagsChanged.Count, "Unexpected number of FlagsChanged events"); - Assert.AreEqual (8, modSeqChanged.Count, "Unexpected number of ModSeqChanged events"); - for (int i = 0; i < modSeqChanged.Count; i++) { - Assert.AreEqual (i, modSeqChanged[i].Index, "Unexpected modSeqChanged[{0}].Index", i); - Assert.AreEqual (i + 1, modSeqChanged[i].UniqueId.Value.Id, "Unexpected modSeqChanged[{0}].UniqueId", i); - Assert.AreEqual (3, modSeqChanged[i].ModSeq, "Unexpected modSeqChanged[{0}].ModSeq", i); + // disable ID support + client.Capabilities &= ~ImapCapabilities.Id; + Assert.ThrowsAsync (() => client.IdentifyAsync (null)); + + await client.DisconnectAsync (false); + } + } + + static List CreateIdleCommands () + { + return new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt"), + new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "gmail.authenticate.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-inbox.txt"), + new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt"), + new ImapReplayCommand ("A00000005 SELECT INBOX (CONDSTORE)\r\n", "gmail.select-inbox.txt"), + new ImapReplayCommand ("A00000006 IDLE\r\n", "gmail.idle.txt"), + new ImapReplayCommand ("A00000006", "DONE\r\n", "gmail.idle-done.txt"), + new ImapReplayCommand ("A00000007 LOGOUT\r\n", "gmail.logout.txt") + }; + } + + [Test] + public void TestIdle () + { + var commands = CreateIdleCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); } - Assert.IsFalse (recentChanged, "Unexpected RecentChanged event"); - modSeqChanged.Clear (); - flagsChanged.Clear (); - var answered = new UniqueIdSet (SortOrder.Ascending); - answered.Add (appended[0]); // A - answered.Add (appended[1]); // B - answered.Add (appended[2]); // C - await folder.AddFlagsAsync (answered, MessageFlags.Answered, true); - Assert.AreEqual (0, flagsChanged.Count, "Unexpected number of FlagsChanged events"); - Assert.AreEqual (3, modSeqChanged.Count, "Unexpected number of ModSeqChanged events"); - for (int i = 0; i < modSeqChanged.Count; i++) { - Assert.AreEqual (i, modSeqChanged[i].Index, "Unexpected modSeqChanged[{0}].Index", i); - Assert.AreEqual (i + 1, modSeqChanged[i].UniqueId.Value.Id, "Unexpected modSeqChanged[{0}].UniqueId", i); - Assert.AreEqual (4, modSeqChanged[i].ModSeq, "Unexpected modSeqChanged[{0}].ModSeq", i); + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); } - Assert.IsFalse (recentChanged, "Unexpected RecentChanged event"); - modSeqChanged.Clear (); - flagsChanged.Clear (); - // Delete some messages so we can test that QRESYNC emits some MessageVanished events - // both now *and* when we use QRESYNC to re-open the folder - var deleted = new UniqueIdSet (SortOrder.Ascending); - deleted.Add (appended[7]); // H - await folder.AddFlagsAsync (deleted, MessageFlags.Deleted, true); - Assert.AreEqual (0, flagsChanged.Count, "Unexpected number of FlagsChanged events"); - Assert.AreEqual (1, modSeqChanged.Count, "Unexpected number of ModSeqChanged events"); - Assert.AreEqual (7, modSeqChanged[0].Index, "Unexpected modSeqChanged[{0}].Index", 0); - Assert.AreEqual (8, modSeqChanged[0].UniqueId.Value.Id, "Unexpected modSeqChanged[{0}].UniqueId", 0); - Assert.AreEqual (5, modSeqChanged[0].ModSeq, "Unexpected modSeqChanged[{0}].ModSeq", 0); - Assert.IsFalse (recentChanged, "Unexpected RecentChanged event"); - modSeqChanged.Clear (); - flagsChanged.Clear (); + using (var done = new CancellationTokenSource ()) { + Assert.Throws (() => client.Idle (CancellationToken.None)); - await folder.ExpungeAsync (deleted); - Assert.AreEqual (1, vanished.Count, "Expected MessagesVanished event"); - Assert.AreEqual (1, vanished[0].UniqueIds.Count, "Unexpected number of messages vanished"); - Assert.AreEqual (8, vanished[0].UniqueIds[0].Id, "Unexpected UID for vanished message"); - Assert.IsFalse (vanished[0].Earlier, "Expected EARLIER to be false"); - Assert.IsTrue (recentChanged, "Expected RecentChanged event"); - recentChanged = false; - vanished.Clear (); + // Should throw InvalidOperationException until a folder is selected. + Assert.Throws (() => client.Idle (done.Token)); - // Verify that THREAD works correctly - var threaded = await folder.ThreadAsync (ThreadingAlgorithm.References, SearchQuery.All); - Assert.AreEqual (2, threaded.Count, "Unexpected number of root nodes in threaded results"); + var inbox = client.Inbox; - threaded = await folder.ThreadAsync (UniqueIdRange.All, ThreadingAlgorithm.OrderedSubject, SearchQuery.All); - Assert.AreEqual (7, threaded.Count, "Unexpected number of root nodes in threaded results"); + inbox.Open (FolderAccess.ReadWrite); - // UNSELECT the folder so we can re-open it using QRESYNC - await folder.CloseAsync (); + int count = 0, expunged = 0, flags = 0; + bool droppedToZero = false; - // Use QRESYNC to get the changes since last time we opened the folder - access = await folder.OpenAsync (FolderAccess.ReadWrite, uidValidity, highestModSeq, appended); - Assert.AreEqual (FolderAccess.ReadWrite, access, "Expected UnitTests.Messages to be opened in READ-WRITE mode"); - Assert.AreEqual (7, flagsChanged.Count, "Unexpected number of MessageFlagsChanged events"); - Assert.AreEqual (7, modSeqChanged.Count, "Unexpected number of ModSeqChanged events"); - for (int i = 0; i < flagsChanged.Count; i++) { - var messageFlags = MessageFlags.Seen | MessageFlags.Draft; + inbox.MessageExpunged += (o, e) => { + expunged++; + Assert.That (e.Index, Is.EqualTo (0), "Expunged Index"); + }; + inbox.MessageFlagsChanged += (o, e) => { + flags++; + Assert.That (e.Flags, Is.EqualTo (MessageFlags.Answered | MessageFlags.Deleted | MessageFlags.Seen), "Flags"); + }; + inbox.CountChanged += (o, e) => { + count++; - if (i < 3) - messageFlags |= MessageFlags.Answered; + if (inbox.Count == 0) + droppedToZero = true; + else if (droppedToZero && inbox.Count == 1) + done.Cancel (); + }; - Assert.AreEqual (i, flagsChanged[i].Index, "Unexpected value for flagsChanged[{0}].Index", i); - Assert.AreEqual ((uint) (i + 1), flagsChanged[i].UniqueId.Value.Id, "Unexpected value for flagsChanged[{0}].UniqueId", i); - Assert.AreEqual (messageFlags, flagsChanged[i].Flags, "Unexpected value for flagsChanged[{0}].Flags", i); + client.Idle (done.Token); - Assert.AreEqual (i, modSeqChanged[i].Index, "Unexpected value for modSeqChanged[{0}].Index", i); - if (i < 3) - Assert.AreEqual (4, modSeqChanged[i].ModSeq, "Unexpected value for modSeqChanged[{0}].ModSeq", i); - else - Assert.AreEqual (3, modSeqChanged[i].ModSeq, "Unexpected value for modSeqChanged[{0}].ModSeq", i); + Assert.That (expunged, Is.EqualTo (21), "Unexpected number of Expunged events"); + Assert.That (count, Is.EqualTo (2), "Unexpected number of CountChanged events"); + Assert.That (flags, Is.EqualTo (21), "Unexpected number of FlagsChanged events"); + Assert.That (inbox, Has.Count.EqualTo (1), "Count"); } - modSeqChanged.Clear (); - flagsChanged.Clear (); - Assert.AreEqual (1, vanished.Count, "Unexpected number of MessagesVanished events"); - Assert.IsTrue (vanished[0].Earlier, "Expected VANISHED EARLIER"); - Assert.AreEqual (1, vanished[0].UniqueIds.Count, "Unexpected number of messages vanished"); - Assert.AreEqual (8, vanished[0].UniqueIds[0].Id, "Unexpected UID for vanished message"); - vanished.Clear (); + client.Disconnect (true); + } + } - // Use SEARCH and FETCH to get the same info - var searchOptions = SearchOptions.All | SearchOptions.Count | SearchOptions.Min | SearchOptions.Max; - var changed = await folder.SearchAsync (searchOptions, SearchQuery.ChangedSince (highestModSeq)); - Assert.AreEqual (7, changed.UniqueIds.Count, "Unexpected number of UIDs"); - Assert.IsTrue (changed.ModSeq.HasValue, "Expected the ModSeq property to be set"); - Assert.AreEqual (4, changed.ModSeq.Value, "Unexpected ModSeq value"); - Assert.AreEqual (1, changed.Min.Value.Id, "Unexpected Min"); - Assert.AreEqual (7, changed.Max.Value.Id, "Unexpected Max"); - Assert.AreEqual (7, changed.Count, "Unexpected Count"); + [Test] + public async Task TestIdleAsync () + { + var commands = CreateIdleCommands (); - var fetched = await folder.FetchAsync (changed.UniqueIds, MessageSummaryItems.UniqueId | MessageSummaryItems.Flags | MessageSummaryItems.ModSeq); - Assert.AreEqual (7, fetched.Count, "Unexpected number of messages fetched"); - for (int i = 0; i < fetched.Count; i++) { - Assert.AreEqual (i, fetched[i].Index, "Unexpected Index"); - Assert.AreEqual (i + 1, fetched[i].UniqueId.Id, "Unexpected UniqueId"); + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); } - // or... we could just use a single UID FETCH command like so: - fetched = await folder.FetchAsync (UniqueIdRange.All, highestModSeq, MessageSummaryItems.UniqueId | MessageSummaryItems.Flags | MessageSummaryItems.ModSeq); - for (int i = 0; i < fetched.Count; i++) { - Assert.AreEqual (i, fetched[i].Index, "Unexpected Index"); - Assert.AreEqual (i + 1, fetched[i].UniqueId.Id, "Unexpected UniqueId"); - } - Assert.AreEqual (7, fetched.Count, "Unexpected number of messages fetched"); - Assert.AreEqual (1, vanished.Count, "Unexpected number of MessagesVanished events"); - Assert.IsTrue (vanished[0].Earlier, "Expected VANISHED EARLIER"); - Assert.AreEqual (1, vanished[0].UniqueIds.Count, "Unexpected number of messages vanished"); - Assert.AreEqual (8, vanished[0].UniqueIds[0].Id, "Unexpected UID for vanished message"); - vanished.Clear (); + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); - // Use SORT to order by reverse arrival order - var orderBy = new OrderBy[] { new OrderBy (OrderByType.Arrival, SortOrder.Descending) }; - var sorted = await folder.SearchAsync (searchOptions, SearchQuery.All, orderBy); - Assert.AreEqual (7, sorted.UniqueIds.Count, "Unexpected number of UIDs"); - for (int i = 0; i < sorted.UniqueIds.Count; i++) - Assert.AreEqual (7 - i, sorted.UniqueIds[i].Id, "Unexpected value for UniqueId[{0}]", i); - Assert.IsFalse (sorted.ModSeq.HasValue, "Expected the ModSeq property to be null"); - Assert.AreEqual (7, sorted.Min.Value.Id, "Unexpected Min"); - Assert.AreEqual (1, sorted.Max.Value.Id, "Unexpected Max"); - Assert.AreEqual (7, sorted.Count, "Unexpected Count"); + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } - // Verify that optimizing NOT queries works correctly - var uids = await folder.SearchAsync (SearchQuery.Not (SearchQuery.Deleted).And (SearchQuery.Not (SearchQuery.NotSeen))); - Assert.AreEqual (7, uids.Count, "Unexpected number of UIDs"); - for (int i = 0; i < uids.Count; i++) - Assert.AreEqual (i + 1, uids[i].Id, "Unexpected value for uids[{0}]", i); + using (var done = new CancellationTokenSource ()) { + Assert.ThrowsAsync (() => client.IdleAsync (CancellationToken.None)); - // Create a Destination folder to use for copying/moving messages to - var destination = await unitTests.CreateAsync ("Destination", true); - Assert.AreEqual (FolderAttributes.HasNoChildren, destination.Attributes, "Unexpected UnitTests.Destination folder attributes"); + // Should throw InvalidOperationException until a folder is selected. + Assert.ThrowsAsync (() => client.IdleAsync (done.Token)); - // COPY messages to the Destination folder - var copied = await folder.CopyToAsync (uids, destination); - Assert.AreEqual (uids.Count, copied.Source.Count, "Unexpetced Source.Count"); - Assert.AreEqual (uids.Count, copied.Destination.Count, "Unexpetced Destination.Count"); + var inbox = client.Inbox; - // MOVE messages to the Destination folder - var moved = await folder.MoveToAsync (uids, destination); - Assert.AreEqual (uids.Count, copied.Source.Count, "Unexpetced Source.Count"); - Assert.AreEqual (uids.Count, copied.Destination.Count, "Unexpetced Destination.Count"); - Assert.AreEqual (1, vanished.Count, "Expected VANISHED event"); - vanished.Clear (); + await inbox.OpenAsync (FolderAccess.ReadWrite); - await destination.StatusAsync (statusItems); - Assert.AreEqual (moved.Destination[0].Validity, destination.UidValidity, "Unexpected UIDVALIDITY"); + int count = 0, expunged = 0, flags = 0; + bool droppedToZero = false; - destination.MessageFlagsChanged += (sender, e) => { - flagsChanged.Add (e); - }; + inbox.MessageExpunged += (o, e) => { + expunged++; + Assert.That (e.Index, Is.EqualTo (0), "Expunged Index"); + }; + inbox.MessageFlagsChanged += (o, e) => { + flags++; + Assert.That (e.Flags, Is.EqualTo (MessageFlags.Answered | MessageFlags.Deleted | MessageFlags.Seen), "Flags"); + }; + inbox.CountChanged += (o, e) => { + count++; - destination.ModSeqChanged += (sender, e) => { - modSeqChanged.Add (e); - }; + if (inbox.Count == 0) + droppedToZero = true; + else if (droppedToZero && inbox.Count == 1) + done.Cancel (); + }; - destination.MessagesVanished += (sender, e) => { - vanished.Add (e); - }; + await client.IdleAsync (done.Token); - destination.RecentChanged += (sender, e) => { - recentChanged = true; - }; + Assert.That (expunged, Is.EqualTo (21), "Unexpected number of Expunged events"); + Assert.That (count, Is.EqualTo (2), "Unexpected number of CountChanged events"); + Assert.That (flags, Is.EqualTo (21), "Unexpected number of FlagsChanged events"); + Assert.That (inbox, Has.Count.EqualTo (1), "Count"); + } - await destination.OpenAsync (FolderAccess.ReadWrite); - Assert.AreEqual (FolderAccess.ReadWrite, access, "Expected UnitTests.Destination to be opened in READ-WRITE mode"); + await client.DisconnectAsync (true); + } + } - var fetchHeaders = new HashSet (); - fetchHeaders.Add (HeaderId.References); - fetchHeaders.Add (HeaderId.XMailer); + static List CreateIdleNotSupportedCommands () + { + return new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt"), + new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "gmail.authenticate.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-inbox.txt"), + new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt"), + new ImapReplayCommand ("A00000005 SELECT INBOX (CONDSTORE)\r\n", "gmail.select-inbox.txt"), + new ImapReplayCommand ("A00000006 LOGOUT\r\n", "gmail.logout.txt") + }; + } - var indexes = new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13 }; + [Test] + public void TestIdleNotSupported () + { + var commands = CreateIdleNotSupportedCommands (); - // Fetch + modseq - fetched = await destination.FetchAsync (UniqueIdRange.All, 1, MessageSummaryItems.Full | MessageSummaryItems.UniqueId | - MessageSummaryItems.BodyStructure | MessageSummaryItems.ModSeq | - MessageSummaryItems.References, fetchHeaders); - Assert.AreEqual (14, fetched.Count, "Unexpected number of messages fetched"); + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } - fetched = await destination.FetchAsync (0, -1, 1, MessageSummaryItems.Full | MessageSummaryItems.UniqueId | - MessageSummaryItems.BodyStructure | MessageSummaryItems.ModSeq | - MessageSummaryItems.References, fetchHeaders); - Assert.AreEqual (14, fetched.Count, "Unexpected number of messages fetched"); + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); - fetched = await destination.FetchAsync (indexes, 1, MessageSummaryItems.Full | MessageSummaryItems.UniqueId | - MessageSummaryItems.BodyStructure | MessageSummaryItems.ModSeq | - MessageSummaryItems.References, fetchHeaders); - Assert.AreEqual (14, fetched.Count, "Unexpected number of messages fetched"); + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } - fetched = await destination.FetchAsync (0, -1, 1, MessageSummaryItems.Full | MessageSummaryItems.UniqueId | - MessageSummaryItems.BodyStructure | MessageSummaryItems.ModSeq | - MessageSummaryItems.References); - Assert.AreEqual (14, fetched.Count, "Unexpected number of messages fetched"); + var inbox = client.Inbox; - fetched = await destination.FetchAsync (indexes, 1, MessageSummaryItems.Full | MessageSummaryItems.UniqueId | - MessageSummaryItems.BodyStructure | MessageSummaryItems.ModSeq | - MessageSummaryItems.References); - Assert.AreEqual (14, fetched.Count, "Unexpected number of messages fetched"); + inbox.Open (FolderAccess.ReadWrite); + + // disable IDLE + client.Capabilities &= ~ImapCapabilities.Idle; + + using (var done = new CancellationTokenSource ()) + Assert.Throws (() => client.Idle (done.Token)); + + client.Disconnect (true); + } + } + + [Test] + public async Task TestIdleNotSupportedAsync () + { + var commands = CreateIdleNotSupportedCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); - // Fetch - fetched = await destination.FetchAsync (UniqueIdRange.All, MessageSummaryItems.Full | MessageSummaryItems.UniqueId | - MessageSummaryItems.BodyStructure | MessageSummaryItems.ModSeq | - MessageSummaryItems.References, fetchHeaders); - Assert.AreEqual (14, fetched.Count, "Unexpected number of messages fetched"); + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } - fetched = await destination.FetchAsync (0, -1, MessageSummaryItems.Full | MessageSummaryItems.UniqueId | - MessageSummaryItems.BodyStructure | MessageSummaryItems.ModSeq | - MessageSummaryItems.References, fetchHeaders); - Assert.AreEqual (14, fetched.Count, "Unexpected number of messages fetched"); + var inbox = client.Inbox; - fetched = await destination.FetchAsync (indexes, MessageSummaryItems.Full | MessageSummaryItems.UniqueId | - MessageSummaryItems.BodyStructure | MessageSummaryItems.ModSeq | - MessageSummaryItems.References, fetchHeaders); - Assert.AreEqual (14, fetched.Count, "Unexpected number of messages fetched"); + await inbox.OpenAsync (FolderAccess.ReadWrite); - fetched = await destination.FetchAsync (0, -1, MessageSummaryItems.Full | MessageSummaryItems.UniqueId | - MessageSummaryItems.BodyStructure | MessageSummaryItems.ModSeq | - MessageSummaryItems.References); - Assert.AreEqual (14, fetched.Count, "Unexpected number of messages fetched"); + // disable IDLE + client.Capabilities &= ~ImapCapabilities.Idle; - fetched = await destination.FetchAsync (indexes, MessageSummaryItems.Full | MessageSummaryItems.UniqueId | - MessageSummaryItems.BodyStructure | MessageSummaryItems.ModSeq | - MessageSummaryItems.References); - Assert.AreEqual (14, fetched.Count, "Unexpected number of messages fetched"); + using (var done = new CancellationTokenSource ()) + Assert.ThrowsAsync (() => client.IdleAsync (done.Token)); - uids = new UniqueIdSet (SortOrder.Ascending); + await client.DisconnectAsync (true); + } + } - for (int i = 0; i < fetched.Count; i++) { - Assert.AreEqual (i, fetched[i].Index, "Unexpected Index"); - Assert.AreEqual (i + 1, fetched[i].UniqueId.Id, "Unexpected UniqueId"); + // TODO: test MessageNew w/ headers + static List CreateNotifyCommands () + { + return new List { + new ImapReplayCommand ("", "dovecot.greeting-preauth.txt"), + new ImapReplayCommand ("A00000000 NAMESPACE\r\n", "dovecot.namespace.txt"), + new ImapReplayCommand ("A00000001 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-inbox.txt"), + new ImapReplayCommand ("A00000002 LIST (SPECIAL-USE) \"\" \"*\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-special-use.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"%\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.notify-list-personal.txt"), + new ImapReplayCommand ("A00000004 EXAMINE Folder (CONDSTORE)\r\n", "dovecot.examine-folder.txt"), + new ImapReplayCommand ("A00000005 NOTIFY SET STATUS (PERSONAL (MailboxName SubscriptionChange)) (SELECTED (MessageNew (UID FLAGS ENVELOPE BODYSTRUCTURE MODSEQ) MessageExpunge FlagChange)) (SUBTREE (INBOX Folder) (MessageNew MessageExpunge MailboxMetadataChange ServerMetadataChange))\r\n", "dovecot.notify.txt"), + new ImapReplayCommand ("A00000006 IDLE\r\n", "dovecot.notify-idle.txt"), + new ImapReplayCommand ("A00000006", "DONE\r\n", "dovecot.notify-idle-done.txt"), + new ImapReplayCommand ("A00000007 NOTIFY NONE\r\n", ImapReplayCommandResponse.OK), + new ImapReplayCommand ("A00000008 NOTIFY SET STATUS (SELECTED (MessageNew (UID FLAGS ENVELOPE BODYSTRUCTURE MODSEQ BODY.PEEK[HEADER.FIELDS (REFERENCES)]) MessageExpunge FlagChange)) (MAILBOXES INBOX (MessageNew MessageExpunge MailboxMetadataChange ServerMetadataChange))\r\n", "dovecot.notify.txt"), + new ImapReplayCommand ("A00000009 NOTIFY NONE\r\n", ImapReplayCommandResponse.OK), + new ImapReplayCommand ("A00000010 LOGOUT\r\n", "gmail.logout.txt") + }; + } - uids.Add (fetched[i].UniqueId); + [Test] + public void TestNotify () + { + const MessageSummaryItems items = MessageSummaryItems.UniqueId | MessageSummaryItems.Envelope | MessageSummaryItems.BodyStructure | MessageSummaryItems.Flags | MessageSummaryItems.ModSeq; + var commands = CreateNotifyCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); } - var entity = await destination.GetBodyPartAsync (fetched[0].UniqueId, fetched[0].TextBody); - Assert.IsInstanceOf (entity); + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); - entity = await destination.GetBodyPartAsync (fetched[0].Index, fetched[0].TextBody); - Assert.IsInstanceOf (entity); + var personal = client.GetFolder (client.PersonalNamespaces[0]); + var folders = personal.GetSubfolders (); + var inbox = client.Inbox; - var headers = await destination.GetHeadersAsync (fetched[0].UniqueId); - Assert.AreEqual ("Unit Tests ", headers[HeaderId.From], "GetHeaders(UniqueId) failed to match From header"); - Assert.AreEqual ("Sun, 02 Oct 2016 17:56:45 -0400", headers[HeaderId.Date], "GetHeaders(UniqueId) failed to match Date header"); - Assert.AreEqual ("A", headers[HeaderId.Subject], "GetHeaders(UniqueId) failed to match Subject header"); - Assert.AreEqual ("", headers[HeaderId.MessageId], "GetHeaders(UniqueId) failed to match Message-Id header"); - Assert.AreEqual ("Unit Tests ", headers[HeaderId.To], "GetHeaders(UniqueId) failed to match To header"); - Assert.AreEqual ("1.0", headers[HeaderId.MimeVersion], "GetHeaders(UniqueId) failed to match MIME-Version header"); - Assert.AreEqual ("text/plain; charset=utf-8", headers[HeaderId.ContentType], "GetHeaders(UniqueId) failed to match Content-Type header"); + var folder = folders.FirstOrDefault (x => x.Name == "Folder"); + var deleteMe = folders.FirstOrDefault (x => x.Name == "DeleteMe"); + var renameMe = folders.FirstOrDefault (x => x.Name == "RenameMe"); + var subscribeMe = folders.FirstOrDefault (x => x.Name == "SubscribeMe"); + var unsubscribeMe = folders.FirstOrDefault (x => x.Name == "UnsubscribeMe"); + + folder.Open (FolderAccess.ReadOnly); + + client.Notify (true, new List { + new ImapEventGroup (ImapMailboxFilter.Personal, new List { + ImapEvent.MailboxName, + ImapEvent.SubscriptionChange + }), + new ImapEventGroup (ImapMailboxFilter.Selected, new List { + new ImapEvent.MessageNew (items), + ImapEvent.MessageExpunge, + ImapEvent.FlagChange + }), + new ImapEventGroup (new ImapMailboxFilter.Subtree (inbox, folder), new List { + new ImapEvent.MessageNew (new FetchRequest ()), + ImapEvent.MessageExpunge, + ImapEvent.MailboxMetadataChange, + ImapEvent.ServerMetadataChange + }), + }); + + // Passing true to notify will update Count + Assert.That (inbox, Has.Count.EqualTo (1), "Messages in INBOX"); + Assert.That (folder, Has.Count.EqualTo (0), "Messages in Folder"); + + IMessageSummary fetched = null; + var folderMessageSummaryFetched = 0; + var folderCountChanged = 0; + var folderFlagsChanged = 0; + + var inboxHighestModSeqChanged = 0; + var inboxMetadataChanged = 0; + var inboxCountChanged = 0; + var metadataChanged = 0; + var unsubscribed = 0; + var subscribed = 0; + var created = 0; + var deleted = 0; + var renamed = 0; + + client.FolderCreated += (sender, e) => { + Assert.That (e.Folder.FullName, Is.EqualTo ("NewFolder"), "e.Folder.FullName"); + Assert.That (e.Folder.Attributes, Is.EqualTo (FolderAttributes.HasNoChildren), "e.Folder.Attributes"); + created++; + }; - headers = await destination.GetHeadersAsync (fetched[0].Index); - Assert.AreEqual ("Unit Tests ", headers[HeaderId.From], "GetHeaders(int) failed to match From header"); - Assert.AreEqual ("Sun, 02 Oct 2016 17:56:45 -0400", headers[HeaderId.Date], "GetHeaders(UniqueId) failed to match Date header"); - Assert.AreEqual ("A", headers[HeaderId.Subject], "GetHeaders(UniqueId) failed to match Subject header"); - Assert.AreEqual ("", headers[HeaderId.MessageId], "GetHeaders(UniqueId) failed to match Message-Id header"); - Assert.AreEqual ("Unit Tests ", headers[HeaderId.To], "GetHeaders(UniqueId) failed to match To header"); - Assert.AreEqual ("1.0", headers[HeaderId.MimeVersion], "GetHeaders(UniqueId) failed to match MIME-Version header"); - Assert.AreEqual ("text/plain; charset=utf-8", headers[HeaderId.ContentType], "GetHeaders(UniqueId) failed to match Content-Type header"); + client.MetadataChanged += (sender, e) => { + Assert.That (e.Metadata.Tag.Id, Is.EqualTo ("/private/comment"), "Metadata.Tag"); + Assert.That (e.Metadata.Value, Is.EqualTo ("this is a comment"), "Metadata.Value"); + metadataChanged++; + }; - headers = await destination.GetHeadersAsync (fetched[0].UniqueId, fetched[0].TextBody); - Assert.AreEqual ("Unit Tests ", headers[HeaderId.From], "GetHeaders(UniqueId, BodyPart) failed to match From header"); - Assert.AreEqual ("Sun, 02 Oct 2016 17:56:45 -0400", headers[HeaderId.Date], "GetHeaders(UniqueId) failed to match Date header"); - Assert.AreEqual ("A", headers[HeaderId.Subject], "GetHeaders(UniqueId) failed to match Subject header"); - Assert.AreEqual ("", headers[HeaderId.MessageId], "GetHeaders(UniqueId) failed to match Message-Id header"); - Assert.AreEqual ("Unit Tests ", headers[HeaderId.To], "GetHeaders(UniqueId) failed to match To header"); - Assert.AreEqual ("1.0", headers[HeaderId.MimeVersion], "GetHeaders(UniqueId) failed to match MIME-Version header"); - Assert.AreEqual ("text/plain; charset=utf-8", headers[HeaderId.ContentType], "GetHeaders(UniqueId) failed to match Content-Type header"); + inbox.MetadataChanged += (sender, e) => { + Assert.That (e.Metadata.Tag.Id, Is.EqualTo ("/private/comment"), "Metadata.Tag"); + Assert.That (e.Metadata.Value, Is.EqualTo ("this is a comment"), "Metadata.Value"); + inboxMetadataChanged++; + }; - headers = await destination.GetHeadersAsync (fetched[0].Index, fetched[0].TextBody); - Assert.AreEqual ("Unit Tests ", headers[HeaderId.From], "GetHeaders(int, BodyPart) failed to match From header"); - Assert.AreEqual ("Sun, 02 Oct 2016 17:56:45 -0400", headers[HeaderId.Date], "GetHeaders(UniqueId) failed to match Date header"); - Assert.AreEqual ("A", headers[HeaderId.Subject], "GetHeaders(UniqueId) failed to match Subject header"); - Assert.AreEqual ("", headers[HeaderId.MessageId], "GetHeaders(UniqueId) failed to match Message-Id header"); - Assert.AreEqual ("Unit Tests ", headers[HeaderId.To], "GetHeaders(UniqueId) failed to match To header"); - Assert.AreEqual ("1.0", headers[HeaderId.MimeVersion], "GetHeaders(UniqueId) failed to match MIME-Version header"); - Assert.AreEqual ("text/plain; charset=utf-8", headers[HeaderId.ContentType], "GetHeaders(UniqueId) failed to match Content-Type header"); + deleteMe.Deleted += (sender, e) => { + deleted++; + }; - using (var stream = await destination.GetStreamAsync (fetched[0].UniqueId, 128, 64)) { - Assert.AreEqual (64, stream.Length, "Unexpected stream length"); + renameMe.Renamed += (sender, e) => { + Assert.That (renameMe.FullName, Is.EqualTo ("RenamedFolder"), "renameMe.FullName"); + renamed++; + }; - string text; - using (var reader = new StreamReader (stream)) - text = reader.ReadToEnd (); + subscribeMe.Subscribed += (sender, e) => { + subscribed++; + }; - Assert.AreEqual ("nit Tests \r\nMIME-Version: 1.0\r\nContent-T", text); - } + unsubscribeMe.Unsubscribed += (sender, e) => { + unsubscribed++; + }; - using (var stream = await destination.GetStreamAsync (fetched[0].UniqueId, "", 128, 64)) { - Assert.AreEqual (64, stream.Length, "Unexpected stream length"); + inbox.HighestModSeqChanged += (sender, e) => { + inboxHighestModSeqChanged++; + }; - string text; - using (var reader = new StreamReader (stream)) - text = reader.ReadToEnd (); + inbox.CountChanged += (sender, e) => { + inboxCountChanged++; + }; - Assert.AreEqual ("nit Tests \r\nMIME-Version: 1.0\r\nContent-T", text); - } + folder.MessageSummaryFetched += (sender, e) => { + folderMessageSummaryFetched++; + fetched = e.Message; + }; - using (var stream = await destination.GetStreamAsync (fetched[0].Index, 128, 64)) { - Assert.AreEqual (64, stream.Length, "Unexpected stream length"); + folder.MessageFlagsChanged += (sender, e) => { + folderFlagsChanged++; + }; - string text; - using (var reader = new StreamReader (stream)) - text = reader.ReadToEnd (); + folder.CountChanged += (sender, e) => { + folderCountChanged++; + }; + + using (var done = new CancellationTokenSource ()) { + folder.CountChanged += (o, e) => { + done.Cancel (); + }; - Assert.AreEqual ("nit Tests \r\nMIME-Version: 1.0\r\nContent-T", text); + client.Idle (done.Token); } - using (var stream = await destination.GetStreamAsync (fetched[0].Index, "", 128, 64)) { - Assert.AreEqual (64, stream.Length, "Unexpected stream length"); + Assert.That (inbox, Has.Count.EqualTo (3), "Inbox.Count"); + Assert.That (inbox.Unread, Is.EqualTo (3), "Inbox.Unread"); + Assert.That (inbox.UidNext.Value.Id, Is.EqualTo (4), "Inbox.UidNext"); + Assert.That (inbox.HighestModSeq, Is.EqualTo (3), "Inbox.HighestModSeq"); + + Assert.That (inboxHighestModSeqChanged, Is.EqualTo (1), "Inbox.HighestModSeqChanged"); + Assert.That (inboxMetadataChanged, Is.EqualTo (1), "Inbox.MetadataChanged"); + Assert.That (inboxCountChanged, Is.EqualTo (1), "Inbox.CountChanged"); + + Assert.That (created, Is.EqualTo (1), "FolderCreated"); + Assert.That (deleted, Is.EqualTo (1), "deleteMe.Deleted"); + Assert.That (renamed, Is.EqualTo (1), "renameMe.Renamed"); + Assert.That (subscribed, Is.EqualTo (1), "subscribeMe.Deleted"); + Assert.That (unsubscribed, Is.EqualTo (1), "unsubscribeMe.Renamed"); + Assert.That (metadataChanged, Is.EqualTo (1), "metadataChanged"); + + Assert.That (folder, Has.Count.EqualTo (1), "Folder.Count"); + Assert.That (folderCountChanged, Is.EqualTo (1), "Folder.CountChanged"); + Assert.That (folderFlagsChanged, Is.EqualTo (1), "Folder.MessageFlagsChanged"); + Assert.That (folderMessageSummaryFetched, Is.EqualTo (1), "Folder.MessageSummaryFetched"); + + Assert.That (fetched.UniqueId.Id, Is.EqualTo (1), "fetched.UniqueId"); + Assert.That (fetched.Flags.Value, Is.EqualTo (MessageFlags.Recent), "fetched.Flags"); + Assert.That (fetched.Envelope.Subject, Is.EqualTo ("IMAP4rev1 WG mtg summary and minutes"), "fetched.Envelope.Subject"); + var body = fetched.Body as BodyPartBasic; + Assert.That (fetched.Body, Is.Not.Null, "fetched.Body"); + Assert.That (body.Octets, Is.EqualTo (3028), "fetched.Body.Octets"); + Assert.That (fetched.ModSeq.Value, Is.EqualTo (1), "fetched.ModSeq"); + + client.DisableNotify (); + + client.Notify (true, new List { + new ImapEventGroup (ImapMailboxFilter.Selected, new List { + new ImapEvent.MessageNew (items | MessageSummaryItems.References), + ImapEvent.MessageExpunge, + ImapEvent.FlagChange + }), + new ImapEventGroup (new ImapMailboxFilter.Mailboxes (inbox), new List { + new ImapEvent.MessageNew (), + ImapEvent.MessageExpunge, + ImapEvent.MailboxMetadataChange, + ImapEvent.ServerMetadataChange + }), + }); + + client.DisableNotify (); + + client.Disconnect (true); + } + } - string text; - using (var reader = new StreamReader (stream)) - text = reader.ReadToEnd (); + [Test] + public async Task TestNotifyAsync () + { + const MessageSummaryItems items = MessageSummaryItems.UniqueId | MessageSummaryItems.Envelope | MessageSummaryItems.BodyStructure | MessageSummaryItems.Flags | MessageSummaryItems.ModSeq; + var commands = CreateNotifyCommands (); - Assert.AreEqual ("nit Tests \r\nMIME-Version: 1.0\r\nContent-T", text); + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); } - using (var stream = await destination.GetStreamAsync (fetched[0].UniqueId, "HEADER.FIELDS (MIME-VERSION CONTENT-TYPE)")) { - Assert.AreEqual (62, stream.Length, "Unexpected stream length"); - - string text; - using (var reader = new StreamReader (stream)) - text = reader.ReadToEnd (); + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); - Assert.AreEqual ("MIME-Version: 1.0\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n", text); - } + var personal = client.GetFolder (client.PersonalNamespaces[0]); + var folders = await personal.GetSubfoldersAsync (); + var inbox = client.Inbox; - using (var stream = await destination.GetStreamAsync (fetched[0].Index, "HEADER.FIELDS (MIME-VERSION CONTENT-TYPE)")) { - Assert.AreEqual (62, stream.Length, "Unexpected stream length"); + var folder = folders.FirstOrDefault (x => x.Name == "Folder"); + var deleteMe = folders.FirstOrDefault (x => x.Name == "DeleteMe"); + var renameMe = folders.FirstOrDefault (x => x.Name == "RenameMe"); + var subscribeMe = folders.FirstOrDefault (x => x.Name == "SubscribeMe"); + var unsubscribeMe = folders.FirstOrDefault (x => x.Name == "UnsubscribeMe"); + + await folder.OpenAsync (FolderAccess.ReadOnly); + + await client.NotifyAsync (true, new List { + new ImapEventGroup (ImapMailboxFilter.Personal, new List { + ImapEvent.MailboxName, + ImapEvent.SubscriptionChange + }), + new ImapEventGroup (ImapMailboxFilter.Selected, new List { + new ImapEvent.MessageNew (items), + ImapEvent.MessageExpunge, + ImapEvent.FlagChange + }), + new ImapEventGroup (new ImapMailboxFilter.Subtree (inbox, folder), new List { + new ImapEvent.MessageNew (new FetchRequest ()), + ImapEvent.MessageExpunge, + ImapEvent.MailboxMetadataChange, + ImapEvent.ServerMetadataChange + }), + }); + + // Passing true to notify will update Count + Assert.That (inbox, Has.Count.EqualTo (1), "Messages in INBOX"); + Assert.That (folder, Has.Count.EqualTo (0), "Messages in Folder"); + + IMessageSummary fetched = null; + var folderMessageSummaryFetched = 0; + var folderCountChanged = 0; + var folderFlagsChanged = 0; + + var inboxHighestModSeqChanged = 0; + var inboxMetadataChanged = 0; + var inboxCountChanged = 0; + var metadataChanged = 0; + var unsubscribed = 0; + var subscribed = 0; + var created = 0; + var deleted = 0; + var renamed = 0; + + client.FolderCreated += (sender, e) => { + Assert.That (e.Folder.FullName, Is.EqualTo ("NewFolder"), "e.Folder.FullName"); + Assert.That (e.Folder.Attributes, Is.EqualTo (FolderAttributes.HasNoChildren), "e.Folder.Attributes"); + created++; + }; - string text; - using (var reader = new StreamReader (stream)) - text = reader.ReadToEnd (); + client.MetadataChanged += (sender, e) => { + Assert.That (e.Metadata.Tag.Id, Is.EqualTo ("/private/comment"), "Metadata.Tag"); + Assert.That (e.Metadata.Value, Is.EqualTo ("this is a comment"), "Metadata.Value"); + metadataChanged++; + }; - Assert.AreEqual ("MIME-Version: 1.0\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n", text); - } + inbox.MetadataChanged += (sender, e) => { + Assert.That (e.Metadata.Tag.Id, Is.EqualTo ("/private/comment"), "Metadata.Tag"); + Assert.That (e.Metadata.Value, Is.EqualTo ("this is a comment"), "Metadata.Value"); + inboxMetadataChanged++; + }; - var custom = new HashSet (); - custom.Add ("$MailKit"); + deleteMe.Deleted += (sender, e) => { + deleted++; + }; - await destination.AddFlagsAsync (uids, destination.HighestModSeq, MessageFlags.Deleted, custom, true); - Assert.AreEqual (14, modSeqChanged.Count, "Unexpected number of ModSeqChanged events"); - Assert.AreEqual (5, destination.HighestModSeq); - for (int i = 0; i < modSeqChanged.Count; i++) { - Assert.AreEqual (i, modSeqChanged[i].Index, "Unexpected value for modSeqChanged[{0}].Index", i); - Assert.AreEqual (5, modSeqChanged[i].ModSeq, "Unexpected value for modSeqChanged[{0}].ModSeq", i); - } - modSeqChanged.Clear (); + renameMe.Renamed += (sender, e) => { + Assert.That (renameMe.FullName, Is.EqualTo ("RenamedFolder"), "renameMe.FullName"); + renamed++; + }; - await destination.SetFlagsAsync (new int[] { 0, 1, 2, 3, 4, 5, 6 }, destination.HighestModSeq, MessageFlags.Seen | MessageFlags.Deleted, custom, true); - Assert.AreEqual (7, modSeqChanged.Count, "Unexpected number of ModSeqChanged events"); - Assert.AreEqual (6, destination.HighestModSeq); - for (int i = 0; i < modSeqChanged.Count; i++) { - Assert.AreEqual (i, modSeqChanged[i].Index, "Unexpected value for modSeqChanged[{0}].Index", i); - Assert.AreEqual (6, modSeqChanged[i].ModSeq, "Unexpected value for modSeqChanged[{0}].ModSeq", i); - } - modSeqChanged.Clear (); + subscribeMe.Subscribed += (sender, e) => { + subscribed++; + }; - var results = await destination.SearchAsync (uids, SearchQuery.Answered.Or (SearchQuery.Deleted.Or (SearchQuery.Draft.Or (SearchQuery.Flagged.Or (SearchQuery.Recent.Or (SearchQuery.NotAnswered.Or (SearchQuery.NotDeleted.Or (SearchQuery.NotDraft.Or (SearchQuery.NotFlagged.Or (SearchQuery.NotSeen.Or (SearchQuery.HasCustomFlag ("$MailKit").Or (SearchQuery.DoesNotHaveCustomFlag ("$MailKit"))))))))))))); - Assert.AreEqual (14, results.Count, "Unexpected number of UIDs"); + unsubscribeMe.Unsubscribed += (sender, e) => { + unsubscribed++; + }; - var matches = await destination.SearchAsync (searchOptions, uids, SearchQuery.LargerThan (256).And (SearchQuery.SmallerThan (512))); - var expectedMatchedUids = new uint[] { 2, 3, 4, 5, 6, 9, 10, 11, 12, 13 }; - Assert.AreEqual (10, matches.Count, "Unexpected COUNT"); - Assert.AreEqual (13, matches.Max.Value.Id, "Unexpected MAX"); - Assert.AreEqual (2, matches.Min.Value.Id, "Unexpected MIN"); - Assert.AreEqual (10, matches.UniqueIds.Count, "Unexpected number of UIDs"); - for (int i = 0; i < matches.UniqueIds.Count; i++) - Assert.AreEqual (expectedMatchedUids[i], matches.UniqueIds[i].Id); + inbox.HighestModSeqChanged += (sender, e) => { + inboxHighestModSeqChanged++; + }; - orderBy = new OrderBy[] { OrderBy.ReverseDate, OrderBy.Subject, OrderBy.DisplayFrom, OrderBy.Size }; - var sentDateQuery = SearchQuery.Or (SearchQuery.And (SearchQuery.SentBefore (new DateTime (2016, 10, 12)), SearchQuery.SentAfter (new DateTime (2016, 10, 10))), SearchQuery.Not (SearchQuery.SentOn (new DateTime (2016, 10, 11)))); - var deliveredDateQuery = SearchQuery.Or (SearchQuery.And (SearchQuery.DeliveredBefore (new DateTime (2016, 10, 12)), SearchQuery.DeliveredAfter (new DateTime (2016, 10, 10))), SearchQuery.Not (SearchQuery.DeliveredOn (new DateTime (2016, 10, 11)))); - results = await destination.SearchAsync (sentDateQuery.Or (deliveredDateQuery), orderBy); - var expectedSortByDateResults = new uint[] { 7, 14, 6, 13, 5, 12, 4, 11, 3, 10, 2, 9, 1, 8 }; - Assert.AreEqual (14, results.Count, "Unexpected number of UIDs"); - for (int i = 0; i < results.Count; i++) - Assert.AreEqual (expectedSortByDateResults[i], results[i].Id); + inbox.CountChanged += (sender, e) => { + inboxCountChanged++; + }; - var stringQuery = SearchQuery.BccContains ("xyz").Or (SearchQuery.CcContains ("xyz").Or (SearchQuery.FromContains ("xyz").Or (SearchQuery.ToContains ("xyz").Or (SearchQuery.SubjectContains ("xyz").Or (SearchQuery.HeaderContains ("Message-Id", "mimekit.net").Or (SearchQuery.BodyContains ("This is the message body.").Or (SearchQuery.MessageContains ("message")))))))); - orderBy = new OrderBy[] { OrderBy.From, OrderBy.To, OrderBy.Cc }; - results = await destination.SearchAsync (uids, stringQuery, orderBy); - Assert.AreEqual (14, results.Count, "Unexpected number of UIDs"); - for (int i = 0; i < results.Count; i++) - Assert.AreEqual (i + 1, results[i].Id); + folder.MessageSummaryFetched += (sender, e) => { + folderMessageSummaryFetched++; + fetched = e.Message; + }; - orderBy = new OrderBy[] { OrderBy.DisplayTo }; - matches = await destination.SearchAsync (searchOptions, uids, SearchQuery.OlderThan (1).And (SearchQuery.YoungerThan (3600)), orderBy); - Assert.AreEqual (14, matches.Count, "Unexpected COUNT"); - Assert.AreEqual (14, matches.Max.Value.Id, "Unexpected MAX"); - Assert.AreEqual (1, matches.Min.Value.Id, "Unexpected MIN"); - Assert.AreEqual (14, matches.UniqueIds.Count, "Unexpected number of UIDs"); - for (int i = 0; i < matches.UniqueIds.Count; i++) - Assert.AreEqual (i + 1, matches.UniqueIds[i].Id); + folder.MessageFlagsChanged += (sender, e) => { + folderFlagsChanged++; + }; - client.Capabilities &= ~ImapCapabilities.ESearch; - matches = await ((ImapFolder) destination).SearchAsync ("ALL"); - Assert.IsFalse (matches.Max.HasValue, "MAX should not be set"); - Assert.IsFalse (matches.Min.HasValue, "MIN should not be set"); - Assert.AreEqual (0, matches.Count, "COUNT should not be set"); - Assert.AreEqual (14, matches.UniqueIds.Count); - for (int i = 0; i < matches.UniqueIds.Count; i++) - Assert.AreEqual (i + 1, matches.UniqueIds[i].Id); + folder.CountChanged += (sender, e) => { + folderCountChanged++; + }; - client.Capabilities &= ~ImapCapabilities.ESort; - matches = await ((ImapFolder) destination).SortAsync ("(REVERSE ARRIVAL) US-ASCII ALL"); - Assert.IsFalse (matches.Max.HasValue, "MAX should not be set"); - Assert.IsFalse (matches.Min.HasValue, "MIN should not be set"); - Assert.AreEqual (0, matches.Count, "COUNT should not be set"); - Assert.AreEqual (14, matches.UniqueIds.Count); - for (int i = 0; i < matches.UniqueIds.Count; i++) - Assert.AreEqual (i + 1, matches.UniqueIds[i].Id); + using (var done = new CancellationTokenSource ()) { + folder.CountChanged += (o, e) => { + done.Cancel (); + }; - await destination.ExpungeAsync (); - Assert.AreEqual (7, destination.HighestModSeq); - Assert.AreEqual (1, vanished.Count, "Unexpected number of Vanished events"); - Assert.AreEqual (14, vanished[0].UniqueIds.Count, "Unexpected number of UIDs in Vanished event"); - for (int i = 0; i < vanished[0].UniqueIds.Count; i++) - Assert.AreEqual (i + 1, vanished[0].UniqueIds[i].Id); - Assert.IsFalse (vanished[0].Earlier, "Unexpected value for Earlier"); - vanished.Clear (); + await client.IdleAsync (done.Token); + } - await destination.CloseAsync (true); + Assert.That (inbox, Has.Count.EqualTo (3), "Inbox.Count"); + Assert.That (inbox.Unread, Is.EqualTo (3), "Inbox.Unread"); + Assert.That (inbox.UidNext.Value.Id, Is.EqualTo (4), "Inbox.UidNext"); + Assert.That (inbox.HighestModSeq, Is.EqualTo (3), "Inbox.HighestModSeq"); + + Assert.That (inboxHighestModSeqChanged, Is.EqualTo (1), "Inbox.HighestModSeqChanged"); + Assert.That (inboxMetadataChanged, Is.EqualTo (1), "Inbox.MetadataChanged"); + Assert.That (inboxCountChanged, Is.EqualTo (1), "Inbox.CountChanged"); + + Assert.That (created, Is.EqualTo (1), "FolderCreated"); + Assert.That (deleted, Is.EqualTo (1), "deleteMe.Deleted"); + Assert.That (renamed, Is.EqualTo (1), "renameMe.Renamed"); + Assert.That (subscribed, Is.EqualTo (1), "subscribeMe.Deleted"); + Assert.That (unsubscribed, Is.EqualTo (1), "unsubscribeMe.Renamed"); + Assert.That (metadataChanged, Is.EqualTo (1), "metadataChanged"); + + Assert.That (folder, Has.Count.EqualTo (1), "Folder.Count"); + Assert.That (folderCountChanged, Is.EqualTo (1), "Folder.CountChanged"); + Assert.That (folderFlagsChanged, Is.EqualTo (1), "Folder.MessageFlagsChanged"); + Assert.That (folderMessageSummaryFetched, Is.EqualTo (1), "Folder.MessageSummaryFetched"); + + Assert.That (fetched.UniqueId.Id, Is.EqualTo (1), "fetched.UniqueId"); + Assert.That (fetched.Flags.Value, Is.EqualTo (MessageFlags.Recent), "fetched.Flags"); + Assert.That (fetched.Envelope.Subject, Is.EqualTo ("IMAP4rev1 WG mtg summary and minutes"), "fetched.Envelope.Subject"); + var body = fetched.Body as BodyPartBasic; + Assert.That (fetched.Body, Is.Not.Null, "fetched.Body"); + Assert.That (body.Octets, Is.EqualTo (3028), "fetched.Body.Octets"); + Assert.That (fetched.ModSeq.Value, Is.EqualTo (1), "fetched.ModSeq"); + + await client.DisableNotifyAsync (); + + await client.NotifyAsync (true, new List { + new ImapEventGroup (ImapMailboxFilter.Selected, new List { + new ImapEvent.MessageNew (items | MessageSummaryItems.References), + ImapEvent.MessageExpunge, + ImapEvent.FlagChange + }), + new ImapEventGroup (new ImapMailboxFilter.Mailboxes (inbox), new List { + new ImapEvent.MessageNew (), + ImapEvent.MessageExpunge, + ImapEvent.MailboxMetadataChange, + ImapEvent.ServerMetadataChange + }), + }); + + await client.DisableNotifyAsync (); - await client.DisconnectAsync (false); + await client.DisconnectAsync (true); } } + static List CreateCompressCommands () + { + return new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt"), + new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "gmail.authenticate.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-inbox.txt"), + new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt"), + new ImapReplayCommand ("A00000005 COMPRESS DEFLATE\r\n", ImapReplayCommandResponse.OK), + new ImapReplayCommand ("A00000006 COMPRESS DEFLATE\r\n", Encoding.ASCII.GetBytes ("A00000006 NO [COMPRESSIONACTIVE] DEFLATE active via COMPRESS\r\n"), true), + new ImapReplayCommand ("A00000007 COMPRESS DEFLATE\r\n", Encoding.ASCII.GetBytes ("A00000007 NO Compress failed for an unknown reason.\r\n"), true), + new ImapReplayCommand ("A00000008 SELECT INBOX (CONDSTORE)\r\n", "gmail.select-inbox.txt", true), + new ImapReplayCommand ("A00000009 UID SEARCH RETURN (ALL) ALL\r\n", "gmail.search.txt", true), + new ImapReplayCommand ("A00000010 UID STORE 1:3,5,7:9,11:14,26:29,31,34,41:43,50 +FLAGS.SILENT (\\Deleted)\r\n", ImapReplayCommandResponse.OK, true), + new ImapReplayCommand ("A00000011 UID EXPUNGE 1:3\r\n", "gmail.expunge.txt", true), + new ImapReplayCommand ("A00000012 LOGOUT\r\n", "gmail.logout.txt", true) + }; + } + [Test] - public async void TestImapClientGMail () + public void TestCompress () { - var commands = new List (); - commands.Add (new ImapReplayCommand ("", "gmail.greeting.txt")); - commands.Add (new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt")); - commands.Add (new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "gmail.authenticate.txt")); - commands.Add (new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt")); - commands.Add (new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\"\r\n", "gmail.list-inbox.txt")); - commands.Add (new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt")); - commands.Add (new ImapReplayCommand ("A00000005 LIST \"\" \"%\"\r\n", "gmail.list-personal.txt")); - commands.Add (new ImapReplayCommand ("A00000006 CREATE UnitTests\r\n", ImapReplayCommandResponse.OK)); - commands.Add (new ImapReplayCommand ("A00000007 LIST \"\" UnitTests\r\n", "gmail.list-unittests.txt")); - commands.Add (new ImapReplayCommand ("A00000008 SELECT UnitTests (CONDSTORE)\r\n", "gmail.select-unittests.txt")); + var commands = CreateCompressCommands (); - for (int i = 0; i < 50; i++) { - MimeMessage message; - string latin1; - long length; + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } - using (var resource = GetResourceStream (string.Format ("common.message.{0}.msg", i))) - message = MimeMessage.Load (resource); + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); - using (var stream = new MemoryStream ()) { - var options = FormatOptions.Default.Clone (); - options.NewLineFormat = NewLineFormat.Dos; + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } - message.WriteTo (options, stream); - length = stream.Length; - stream.Position = 0; + client.Compress (); + client.Compress (); + Assert.Throws (() => client.Compress ()); - using (var reader = new StreamReader (stream, Latin1)) - latin1 = reader.ReadToEnd (); - } + int changed = 0, expunged = 0; + var inbox = client.Inbox; + + inbox.Open (FolderAccess.ReadWrite); - var command = string.Format ("A{0:D8} APPEND UnitTests (\\Seen) ", i + 9); + inbox.MessageExpunged += (o, e) => { expunged++; Assert.That (e.Index, Is.EqualTo (0), "Expunged event message index"); }; + inbox.CountChanged += (o, e) => { changed++; }; - if (length > 4096) { - command += "{" + length + "}\r\n"; - commands.Add (new ImapReplayCommand (command, "gmail.go-ahead.txt")); - commands.Add (new ImapReplayCommand (latin1 + "\r\n", string.Format ("gmail.append.{0}.txt", i + 1))); - } else { - command += "{" + length + "+}\r\n" + latin1 + "\r\n"; - commands.Add (new ImapReplayCommand (command, string.Format ("gmail.append.{0}.txt", i + 1))); - } + var uids = inbox.Search (SearchQuery.All); + inbox.AddFlags (uids, MessageFlags.Deleted, true); + + uids = new UniqueIdRange (0, 1, 3); + inbox.Expunge (uids); + + Assert.That (expunged, Is.EqualTo (3), "Unexpected number of Expunged events"); + Assert.That (changed, Is.EqualTo (1), "Unexpected number of CountChanged events"); + Assert.That (inbox, Has.Count.EqualTo (18), "Count"); + + client.Disconnect (true); } + } - commands.Add (new ImapReplayCommand ("A00000059 UID SEARCH RETURN () OR TO nsb CC nsb\r\n", "gmail.search.txt")); - commands.Add (new ImapReplayCommand ("A00000060 UID FETCH 1:3,5,7:9,11:14,26:29,31,34,41:43,50 (UID FLAGS INTERNALDATE RFC822.SIZE ENVELOPE BODY)\r\n", "gmail.search-summary.txt")); - commands.Add (new ImapReplayCommand ("A00000061 UID FETCH 1 (BODY.PEEK[])\r\n", "gmail.fetch.1.txt")); - commands.Add (new ImapReplayCommand ("A00000062 UID FETCH 2 (BODY.PEEK[])\r\n", "gmail.fetch.2.txt")); - commands.Add (new ImapReplayCommand ("A00000063 UID FETCH 3 (BODY.PEEK[])\r\n", "gmail.fetch.3.txt")); - commands.Add (new ImapReplayCommand ("A00000064 UID FETCH 5 (BODY.PEEK[])\r\n", "gmail.fetch.5.txt")); - commands.Add (new ImapReplayCommand ("A00000065 UID FETCH 7 (BODY.PEEK[])\r\n", "gmail.fetch.7.txt")); - commands.Add (new ImapReplayCommand ("A00000066 UID FETCH 8 (BODY.PEEK[])\r\n", "gmail.fetch.8.txt")); - commands.Add (new ImapReplayCommand ("A00000067 UID FETCH 9 (BODY.PEEK[])\r\n", "gmail.fetch.9.txt")); - commands.Add (new ImapReplayCommand ("A00000068 UID FETCH 11 (BODY.PEEK[])\r\n", "gmail.fetch.11.txt")); - commands.Add (new ImapReplayCommand ("A00000069 UID FETCH 12 (BODY.PEEK[])\r\n", "gmail.fetch.12.txt")); - commands.Add (new ImapReplayCommand ("A00000070 UID FETCH 13 (BODY.PEEK[])\r\n", "gmail.fetch.13.txt")); - commands.Add (new ImapReplayCommand ("A00000071 UID FETCH 14 (BODY.PEEK[])\r\n", "gmail.fetch.14.txt")); - commands.Add (new ImapReplayCommand ("A00000072 UID FETCH 26 (BODY.PEEK[])\r\n", "gmail.fetch.26.txt")); - commands.Add (new ImapReplayCommand ("A00000073 UID FETCH 27 (BODY.PEEK[])\r\n", "gmail.fetch.27.txt")); - commands.Add (new ImapReplayCommand ("A00000074 UID FETCH 28 (BODY.PEEK[])\r\n", "gmail.fetch.28.txt")); - commands.Add (new ImapReplayCommand ("A00000075 UID FETCH 29 (BODY.PEEK[])\r\n", "gmail.fetch.29.txt")); - commands.Add (new ImapReplayCommand ("A00000076 UID FETCH 31 (BODY.PEEK[])\r\n", "gmail.fetch.31.txt")); - commands.Add (new ImapReplayCommand ("A00000077 UID FETCH 34 (BODY.PEEK[])\r\n", "gmail.fetch.34.txt")); - commands.Add (new ImapReplayCommand ("A00000078 UID FETCH 41 (BODY.PEEK[])\r\n", "gmail.fetch.41.txt")); - commands.Add (new ImapReplayCommand ("A00000079 UID FETCH 42 (BODY.PEEK[])\r\n", "gmail.fetch.42.txt")); - commands.Add (new ImapReplayCommand ("A00000080 UID FETCH 43 (BODY.PEEK[])\r\n", "gmail.fetch.43.txt")); - commands.Add (new ImapReplayCommand ("A00000081 UID FETCH 50 (BODY.PEEK[])\r\n", "gmail.fetch.50.txt")); - commands.Add (new ImapReplayCommand ("A00000082 UID STORE 1:3,5,7:9,11:14,26:29,31,34,41:43,50 FLAGS (\\Answered \\Seen)\r\n", "gmail.set-flags.txt")); - commands.Add (new ImapReplayCommand ("A00000083 UID STORE 1:3,5,7:9,11:14,26:29,31,34,41:43,50 -FLAGS.SILENT (\\Answered)\r\n", ImapReplayCommandResponse.OK)); - commands.Add (new ImapReplayCommand ("A00000084 UID STORE 1:3,5,7:9,11:14,26:29,31,34,41:43,50 +FLAGS.SILENT (\\Deleted)\r\n", "gmail.add-flags.txt")); - commands.Add (new ImapReplayCommand ("A00000085 CHECK\r\n", ImapReplayCommandResponse.OK)); - commands.Add (new ImapReplayCommand ("A00000086 UNSELECT\r\n", ImapReplayCommandResponse.OK)); - commands.Add (new ImapReplayCommand ("A00000087 SUBSCRIBE UnitTests\r\n", ImapReplayCommandResponse.OK)); - commands.Add (new ImapReplayCommand ("A00000088 LSUB \"\" \"%\"\r\n", "gmail.lsub-personal.txt")); - commands.Add (new ImapReplayCommand ("A00000089 UNSUBSCRIBE UnitTests\r\n", ImapReplayCommandResponse.OK)); - commands.Add (new ImapReplayCommand ("A00000090 CREATE UnitTests/Dummy\r\n", ImapReplayCommandResponse.OK)); - commands.Add (new ImapReplayCommand ("A00000091 LIST \"\" UnitTests/Dummy\r\n", "gmail.list-unittests-dummy.txt")); - commands.Add (new ImapReplayCommand ("A00000092 RENAME UnitTests RenamedUnitTests\r\n", ImapReplayCommandResponse.OK)); - commands.Add (new ImapReplayCommand ("A00000093 DELETE RenamedUnitTests\r\n", ImapReplayCommandResponse.OK)); - commands.Add (new ImapReplayCommand ("A00000094 LOGOUT\r\n", "gmail.logout.txt")); + [Test] + public async Task TestCompressAsync () + { + var commands = CreateCompressCommands (); - using (var client = new ImapClient ()) { + using (var client = new ImapClient () { TagPrefix = 'A' }) { try { - client.ReplayConnect ("localhost", new ImapReplayStream (commands, false)); + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); } catch (Exception ex) { - Assert.Fail ("Did not expect an exception in Connect: {0}", ex); + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); } - Assert.IsTrue (client.IsConnected, "Client failed to connect."); - - Assert.AreEqual (GMailInitialCapabilities, client.Capabilities); - Assert.AreEqual (5, client.AuthenticationMechanisms.Count); - Assert.IsTrue (client.AuthenticationMechanisms.Contains ("XOAUTH"), "Expected SASL XOAUTH auth mechanism"); - Assert.IsTrue (client.AuthenticationMechanisms.Contains ("XOAUTH2"), "Expected SASL XOAUTH2 auth mechanism"); - Assert.IsTrue (client.AuthenticationMechanisms.Contains ("OAUTHBEARER"), "Expected SASL OAUTHBEARER auth mechanism"); - Assert.IsTrue (client.AuthenticationMechanisms.Contains ("PLAIN"), "Expected SASL PLAIN auth mechanism"); - Assert.IsTrue (client.AuthenticationMechanisms.Contains ("PLAIN-CLIENTTOKEN"), "Expected SASL PLAIN-CLIENTTOKEN auth mechanism"); - - // Note: Do not try XOAUTH2 - client.AuthenticationMechanisms.Remove ("XOAUTH2"); + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); try { await client.AuthenticateAsync ("username", "password"); } catch (Exception ex) { - Assert.Fail ("Did not expect an exception in Authenticate: {0}", ex); + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); } - Assert.AreEqual (GMailAuthenticatedCapabilities, client.Capabilities); - Assert.IsTrue (client.AppendLimit.HasValue, "Expected AppendLimit to have a value"); - Assert.AreEqual (35651584, client.AppendLimit.Value, "Expected AppendLimit value to match"); + await client.CompressAsync (); + await client.CompressAsync (); + Assert.ThrowsAsync (() => client.CompressAsync ()); + int changed = 0, expunged = 0; var inbox = client.Inbox; - Assert.IsNotNull (inbox, "Expected non-null Inbox folder."); - Assert.AreEqual (FolderAttributes.Inbox | FolderAttributes.HasNoChildren, inbox.Attributes, "Expected Inbox attributes to be \\HasNoChildren."); - foreach (var special in Enum.GetValues (typeof (SpecialFolder)).OfType ()) { - var folder = client.GetFolder (special); - - if (special != SpecialFolder.Archive) { - var expected = GetSpecialFolderAttribute (special) | FolderAttributes.HasNoChildren; + await inbox.OpenAsync (FolderAccess.ReadWrite); - Assert.IsNotNull (folder, "Expected non-null {0} folder.", special); - Assert.AreEqual (expected, folder.Attributes, "Expected {0} attributes to be \\HasNoChildren.", special); - } else { - Assert.IsNull (folder, "Expected null {0} folder.", special); - } - } + inbox.MessageExpunged += (o, e) => { expunged++; Assert.That (e.Index, Is.EqualTo (0), "Expunged event message index"); }; + inbox.CountChanged += (o, e) => { changed++; }; - // disable LIST-EXTENDED - client.Capabilities &= ~ImapCapabilities.ListExtended; + var uids = await inbox.SearchAsync (SearchQuery.All); + await inbox.AddFlagsAsync (uids, MessageFlags.Deleted, true); - var personal = client.GetFolder (client.PersonalNamespaces[0]); - var folders = (await personal.GetSubfoldersAsync ()).ToList (); - Assert.AreEqual (client.Inbox, folders[0], "Expected the first folder to be the Inbox."); - Assert.AreEqual ("[Gmail]", folders[1].FullName, "Expected the second folder to be [Gmail]."); - Assert.AreEqual (FolderAttributes.NoSelect | FolderAttributes.HasChildren, folders[1].Attributes, "Expected [Gmail] folder to be \\Noselect \\HasChildren."); + uids = new UniqueIdRange (0, 1, 3); + await inbox.ExpungeAsync (uids); - var created = await personal.CreateAsync ("UnitTests", true); - Assert.IsNotNull (created, "Expected a non-null created folder."); - Assert.AreEqual (FolderAttributes.HasNoChildren, created.Attributes); + Assert.That (expunged, Is.EqualTo (3), "Unexpected number of Expunged events"); + Assert.That (changed, Is.EqualTo (1), "Unexpected number of CountChanged events"); + Assert.That (inbox, Has.Count.EqualTo (18), "Count"); - Assert.IsNotNull (created.ParentFolder, "The ParentFolder property should not be null."); + await client.DisconnectAsync (true); + } + } - const MessageFlags ExpectedPermanentFlags = MessageFlags.Answered | MessageFlags.Flagged | MessageFlags.Draft | MessageFlags.Deleted | MessageFlags.Seen | MessageFlags.UserDefined; - const MessageFlags ExpectedAcceptedFlags = MessageFlags.Answered | MessageFlags.Flagged | MessageFlags.Draft | MessageFlags.Deleted | MessageFlags.Seen; - var access = await created.OpenAsync (FolderAccess.ReadWrite); - Assert.AreEqual (FolderAccess.ReadWrite, access, "The UnitTests folder was not opened with the expected access mode."); - Assert.AreEqual (ExpectedPermanentFlags, created.PermanentFlags, "The PermanentFlags do not match the expected value."); - Assert.AreEqual (ExpectedAcceptedFlags, created.AcceptedFlags, "The AcceptedFlags do not match the expected value."); + static List CreateAccessControlListsCommands () + { + return new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "acl.capability.txt"), + new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "acl.authenticate.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-inbox.txt"), + new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt"), + new ImapReplayCommand ("A00000005 GETACL INBOX\r\n", "acl.getacl.txt"), + new ImapReplayCommand ("A00000006 LISTRIGHTS INBOX smith\r\n", "acl.listrights.txt"), + new ImapReplayCommand ("A00000007 MYRIGHTS INBOX\r\n", "acl.myrights.txt"), + new ImapReplayCommand ("A00000008 SETACL INBOX smith +lrswida\r\n", ImapReplayCommandResponse.OK), + new ImapReplayCommand ("A00000009 SETACL INBOX smith -lrswida\r\n", ImapReplayCommandResponse.OK), + new ImapReplayCommand ("A00000010 SETACL INBOX smith lrswida\r\n", ImapReplayCommandResponse.OK), + new ImapReplayCommand ("A00000011 DELETEACL INBOX smith\r\n", ImapReplayCommandResponse.OK) + }; + } - for (int i = 0; i < 50; i++) { - using (var stream = GetResourceStream (string.Format ("common.message.{0}.msg", i))) { - var message = MimeMessage.Load (stream); + [Test] + public void TestAccessControlLists () + { + var commands = CreateAccessControlListsCommands (); - var uid = await created.AppendAsync (message, MessageFlags.Seen); - Assert.IsTrue (uid.HasValue, "Expected a UID to be returned from folder.Append()."); - Assert.AreEqual ((uint) (i + 1), uid.Value.Id, "The UID returned from the APPEND command does not match the expected UID."); - } + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); } - var query = SearchQuery.ToContains ("nsb").Or (SearchQuery.CcContains ("nsb")); - var matches = await created.SearchAsync (query); + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); - const MessageSummaryItems items = MessageSummaryItems.Full | MessageSummaryItems.UniqueId; - var summaries = await created.FetchAsync (matches, items); + Assert.That (client.Capabilities, Is.EqualTo (AclInitialCapabilities)); + Assert.That (client.AuthenticationMechanisms, Has.Count.EqualTo (4)); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH"), "Expected SASL XOAUTH auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH2"), "Expected SASL XOAUTH2 auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN"), "Expected SASL PLAIN auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN-CLIENTTOKEN"), "Expected SASL PLAIN-CLIENTTOKEN auth mechanism"); + Assert.That (client.Rights.ToString (), Is.EqualTo ("texk"), "Rights"); - foreach (var summary in summaries) { - if (summary.UniqueId.IsValid) - await created.GetMessageAsync (summary.UniqueId); - else - await created.GetMessageAsync (summary.Index); + // Note: Do not try XOAUTH2 + client.AuthenticationMechanisms.Remove ("XOAUTH2"); + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); } - await created.SetFlagsAsync (matches, MessageFlags.Seen | MessageFlags.Answered, false); - await created.RemoveFlagsAsync (matches, MessageFlags.Answered, true); - await created.AddFlagsAsync (matches, MessageFlags.Deleted, true); + Assert.That (client.Capabilities, Is.EqualTo (AclAuthenticatedCapabilities)); - await created.CheckAsync (); + var inbox = client.Inbox; + Assert.That (inbox, Is.Not.Null, "Expected non-null Inbox folder."); + Assert.That (inbox.Attributes, Is.EqualTo (FolderAttributes.Inbox | FolderAttributes.HasNoChildren | FolderAttributes.Subscribed), "Expected Inbox attributes to be \\HasNoChildren."); - await created.CloseAsync (); - Assert.IsFalse (created.IsOpen, "Expected the UnitTests folder to be closed."); + foreach (var special in Enum.GetValues (typeof (SpecialFolder)).OfType ()) { + var folder = client.GetFolder (special); - await created.SubscribeAsync (); - Assert.IsTrue (created.IsSubscribed, "Expected IsSubscribed to be true after subscribing to the folder."); + if (special != SpecialFolder.Archive) { + var expected = GetSpecialFolderAttribute (special) | FolderAttributes.HasNoChildren; - var subscribed = (await personal.GetSubfoldersAsync (true)).ToList (); - Assert.IsTrue (subscribed.Contains (created), "Expected the list of subscribed folders to contain the UnitTests folder."); + Assert.That (folder, Is.Not.Null, $"Expected non-null {special} folder."); + Assert.That (folder.Attributes, Is.EqualTo (expected), $"Expected {special} attributes to be \\HasNoChildren."); + } else { + Assert.That (folder, Is.Null, $"Expected null {special} folder."); + } + } - await created.UnsubscribeAsync (); - Assert.IsFalse (created.IsSubscribed, "Expected IsSubscribed to be false after unsubscribing from the folder."); + // GETACL INBOX + var acl = client.Inbox.GetAccessControlList (); + Assert.That (acl, Has.Count.EqualTo (2), "The number of access controls does not match."); + Assert.That (acl[0].Name, Is.EqualTo ("Fred"), "The identifier for the first access control does not match."); + Assert.That (acl[0].Rights.ToString (), Is.EqualTo ("rwipslxetad"), "The access rights for the first access control does not match."); + Assert.That (acl[1].Name, Is.EqualTo ("Chris"), "The identifier for the second access control does not match."); + Assert.That (acl[1].Rights.ToString (), Is.EqualTo ("lrswi"), "The access rights for the second access control does not match."); - var dummy = await created.CreateAsync ("Dummy", true); - bool dummyRenamed = false; - bool renamed = false; - bool deleted = false; + // LISTRIGHTS INBOX smith + Assert.Throws (() => client.Inbox.GetAccessRights (null)); + //Assert.Throws (() => client.Inbox.GetAccessRights (string.Empty)); + var rights = client.Inbox.GetAccessRights ("smith"); + Assert.That (rights.ToString (), Is.EqualTo ("lrswipkxtecda0123456789"), "The access rights do not match for user smith."); - dummy.Renamed += (sender, e) => { dummyRenamed = true; }; - created.Renamed += (sender, e) => { renamed = true; }; + // MYRIGHTS INBOX + rights = client.Inbox.GetMyAccessRights (); + Assert.That (rights.ToString (), Is.EqualTo ("rwiptsldaex"), "My access rights do not match."); - await created.RenameAsync (created.ParentFolder, "RenamedUnitTests"); - Assert.AreEqual ("RenamedUnitTests", created.Name); - Assert.AreEqual ("RenamedUnitTests", created.FullName); - Assert.IsTrue (renamed, "Expected the Rename event to be emitted for the UnitTests folder."); + // SETACL INBOX smith +lrswida + var empty = new AccessRights (string.Empty); + rights = new AccessRights ("lrswida"); + Assert.Throws (() => client.Inbox.AddAccessRights (null, rights)); + //Assert.Throws (() => client.Inbox.AddAccessRights (string.Empty, rights)); + Assert.Throws (() => client.Inbox.AddAccessRights ("smith", null)); + Assert.Throws (() => client.Inbox.AddAccessRights ("smith", empty)); + client.Inbox.AddAccessRights ("smith", rights); - Assert.AreEqual ("RenamedUnitTests/Dummy", dummy.FullName); - Assert.IsTrue (dummyRenamed, "Expected the Rename event to be emitted for the UnitTests/Dummy folder."); + // SETACL INBOX smith -lrswida + Assert.Throws (() => client.Inbox.RemoveAccessRights (null, rights)); + //Assert.Throws (() => client.Inbox.RemoveAccessRights (string.Empty, rights)); + Assert.Throws (() => client.Inbox.RemoveAccessRights ("smith", null)); + Assert.Throws (() => client.Inbox.RemoveAccessRights ("smith", empty)); + client.Inbox.RemoveAccessRights ("smith", rights); - created.Deleted += (sender, e) => { deleted = true; }; + // SETACL INBOX smith lrswida + Assert.Throws (() => client.Inbox.SetAccessRights (null, rights)); + //Assert.Throws (() => client.Inbox.SetAccessRights (string.Empty, rights)); + Assert.Throws (() => client.Inbox.SetAccessRights ("smith", null)); + client.Inbox.SetAccessRights ("smith", rights); - await created.DeleteAsync (); - Assert.IsTrue (deleted, "Expected the Deleted event to be emitted for the UnitTests folder."); - Assert.IsFalse (created.Exists, "Expected Exists to be false after deleting the folder."); + // DELETEACL INBOX smith + Assert.Throws (() => client.Inbox.RemoveAccess (null)); + //Assert.Throws (() => client.Inbox.RemoveAccess (string.Empty)); + client.Inbox.RemoveAccess ("smith"); - await client.DisconnectAsync (true); + client.Disconnect (false); } } [Test] - public async void TestAccessControlLists () + public async Task TestAccessControlListsAsync () { - var commands = new List (); - commands.Add (new ImapReplayCommand ("", "gmail.greeting.txt")); - commands.Add (new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "acl.capability.txt")); - commands.Add (new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "acl.authenticate.txt")); - commands.Add (new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt")); - commands.Add (new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\"\r\n", "gmail.list-inbox.txt")); - commands.Add (new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt")); - commands.Add (new ImapReplayCommand ("A00000005 GETACL INBOX\r\n", "acl.getacl.txt")); - commands.Add (new ImapReplayCommand ("A00000006 LISTRIGHTS INBOX smith\r\n", "acl.listrights.txt")); - commands.Add (new ImapReplayCommand ("A00000007 MYRIGHTS INBOX\r\n", "acl.myrights.txt")); - commands.Add (new ImapReplayCommand ("A00000008 SETACL INBOX smith +lrswida\r\n", ImapReplayCommandResponse.OK)); - commands.Add (new ImapReplayCommand ("A00000009 SETACL INBOX smith -lrswida\r\n", ImapReplayCommandResponse.OK)); - commands.Add (new ImapReplayCommand ("A00000010 SETACL INBOX smith lrswida\r\n", ImapReplayCommandResponse.OK)); - commands.Add (new ImapReplayCommand ("A00000011 DELETEACL INBOX smith\r\n", ImapReplayCommandResponse.OK)); + var commands = CreateAccessControlListsCommands (); - using (var client = new ImapClient ()) { + using (var client = new ImapClient () { TagPrefix = 'A' }) { try { - client.ReplayConnect ("localhost", new ImapReplayStream (commands, false)); + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); } catch (Exception ex) { - Assert.Fail ("Did not expect an exception in Connect: {0}", ex); + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); } - Assert.IsTrue (client.IsConnected, "Client failed to connect."); + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); - Assert.AreEqual (AclInitialCapabilities, client.Capabilities); - Assert.AreEqual (4, client.AuthenticationMechanisms.Count); - Assert.IsTrue (client.AuthenticationMechanisms.Contains ("XOAUTH"), "Expected SASL XOAUTH auth mechanism"); - Assert.IsTrue (client.AuthenticationMechanisms.Contains ("XOAUTH2"), "Expected SASL XOAUTH2 auth mechanism"); - Assert.IsTrue (client.AuthenticationMechanisms.Contains ("PLAIN"), "Expected SASL PLAIN auth mechanism"); - Assert.IsTrue (client.AuthenticationMechanisms.Contains ("PLAIN-CLIENTTOKEN"), "Expected SASL PLAIN-CLIENTTOKEN auth mechanism"); + Assert.That (client.Capabilities, Is.EqualTo (AclInitialCapabilities)); + Assert.That (client.AuthenticationMechanisms, Has.Count.EqualTo (4)); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH"), "Expected SASL XOAUTH auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH2"), "Expected SASL XOAUTH2 auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN"), "Expected SASL PLAIN auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN-CLIENTTOKEN"), "Expected SASL PLAIN-CLIENTTOKEN auth mechanism"); + Assert.That (client.Rights.ToString (), Is.EqualTo ("texk"), "Rights"); // Note: Do not try XOAUTH2 client.AuthenticationMechanisms.Remove ("XOAUTH2"); @@ -2109,14 +7209,14 @@ public async void TestAccessControlLists () try { await client.AuthenticateAsync ("username", "password"); } catch (Exception ex) { - Assert.Fail ("Did not expect an exception in Authenticate: {0}", ex); + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); } - Assert.AreEqual (AclAuthenticatedCapabilities, client.Capabilities); + Assert.That (client.Capabilities, Is.EqualTo (AclAuthenticatedCapabilities)); var inbox = client.Inbox; - Assert.IsNotNull (inbox, "Expected non-null Inbox folder."); - Assert.AreEqual (FolderAttributes.Inbox | FolderAttributes.HasNoChildren, inbox.Attributes, "Expected Inbox attributes to be \\HasNoChildren."); + Assert.That (inbox, Is.Not.Null, "Expected non-null Inbox folder."); + Assert.That (inbox.Attributes, Is.EqualTo (FolderAttributes.Inbox | FolderAttributes.HasNoChildren | FolderAttributes.Subscribed), "Expected Inbox attributes to be \\HasNoChildren."); foreach (var special in Enum.GetValues (typeof (SpecialFolder)).OfType ()) { var folder = client.GetFolder (special); @@ -2124,135 +7224,126 @@ public async void TestAccessControlLists () if (special != SpecialFolder.Archive) { var expected = GetSpecialFolderAttribute (special) | FolderAttributes.HasNoChildren; - Assert.IsNotNull (folder, "Expected non-null {0} folder.", special); - Assert.AreEqual (expected, folder.Attributes, "Expected {0} attributes to be \\HasNoChildren.", special); + Assert.That (folder, Is.Not.Null, $"Expected non-null {special} folder."); + Assert.That (folder.Attributes, Is.EqualTo (expected), $"Expected {special} attributes to be \\HasNoChildren."); } else { - Assert.IsNull (folder, "Expected null {0} folder.", special); + Assert.That (folder, Is.Null, $"Expected null {special} folder."); } } // GETACL INBOX var acl = await client.Inbox.GetAccessControlListAsync (); - Assert.AreEqual (2, acl.Count, "The number of access controls does not match."); - Assert.AreEqual ("Fred", acl[0].Name, "The identifier for the first access control does not match."); - Assert.AreEqual ("rwipslxetad", acl[0].Rights.ToString (), "The access rights for the first access control does not match."); - Assert.AreEqual ("Chris", acl[1].Name, "The identifier for the second access control does not match."); - Assert.AreEqual ("lrswi", acl[1].Rights.ToString (), "The access rights for the second access control does not match."); + Assert.That (acl, Has.Count.EqualTo (2), "The number of access controls does not match."); + Assert.That (acl[0].Name, Is.EqualTo ("Fred"), "The identifier for the first access control does not match."); + Assert.That (acl[0].Rights.ToString (), Is.EqualTo ("rwipslxetad"), "The access rights for the first access control does not match."); + Assert.That (acl[1].Name, Is.EqualTo ("Chris"), "The identifier for the second access control does not match."); + Assert.That (acl[1].Rights.ToString (), Is.EqualTo ("lrswi"), "The access rights for the second access control does not match."); // LISTRIGHTS INBOX smith - Assert.Throws (() => client.Inbox.GetAccessRights (null)); - //Assert.Throws (() => client.Inbox.GetAccessRights (string.Empty)); - Assert.Throws (async () => await client.Inbox.GetAccessRightsAsync (null)); - //Assert.Throws (async () => await client.Inbox.GetAccessRightsAsync (string.Empty)); + Assert.ThrowsAsync (async () => await client.Inbox.GetAccessRightsAsync (null)); + //Assert.ThrowsAsync (async () => await client.Inbox.GetAccessRightsAsync (string.Empty)); var rights = await client.Inbox.GetAccessRightsAsync ("smith"); - Assert.AreEqual ("lrswipkxtecda0123456789", rights.ToString (), "The access rights do not match for user smith."); + Assert.That (rights.ToString (), Is.EqualTo ("lrswipkxtecda0123456789"), "The access rights do not match for user smith."); // MYRIGHTS INBOX rights = await client.Inbox.GetMyAccessRightsAsync (); - Assert.AreEqual ("rwiptsldaex", rights.ToString (), "My access rights do not match."); + Assert.That (rights.ToString (), Is.EqualTo ("rwiptsldaex"), "My access rights do not match."); // SETACL INBOX smith +lrswida var empty = new AccessRights (string.Empty); rights = new AccessRights ("lrswida"); - Assert.Throws (() => client.Inbox.AddAccessRights (null, rights)); - //Assert.Throws (() => client.Inbox.AddAccessRights (string.Empty, rights)); - Assert.Throws (() => client.Inbox.AddAccessRights ("smith", null)); - Assert.Throws (() => client.Inbox.AddAccessRights ("smith", empty)); - Assert.Throws (async () => await client.Inbox.AddAccessRightsAsync (null, rights)); - //Assert.Throws (async () => await client.Inbox.AddAccessRightsAsync (string.Empty, rights)); - Assert.Throws (async () => await client.Inbox.AddAccessRightsAsync ("smith", null)); - Assert.Throws (async () => await client.Inbox.AddAccessRightsAsync ("smith", empty)); + Assert.ThrowsAsync (async () => await client.Inbox.AddAccessRightsAsync (null, rights)); + //Assert.ThrowsAsync (async () => await client.Inbox.AddAccessRightsAsync (string.Empty, rights)); + Assert.ThrowsAsync (async () => await client.Inbox.AddAccessRightsAsync ("smith", null)); + Assert.ThrowsAsync (async () => await client.Inbox.AddAccessRightsAsync ("smith", empty)); await client.Inbox.AddAccessRightsAsync ("smith", rights); // SETACL INBOX smith -lrswida - Assert.Throws (() => client.Inbox.RemoveAccessRights (null, rights)); - //Assert.Throws (() => client.Inbox.RemoveAccessRights (string.Empty, rights)); - Assert.Throws (() => client.Inbox.RemoveAccessRights ("smith", null)); - Assert.Throws (() => client.Inbox.RemoveAccessRights ("smith", empty)); - Assert.Throws (async () => await client.Inbox.RemoveAccessRightsAsync (null, rights)); - //Assert.Throws (async () => await client.Inbox.RemoveAccessRightsAsync (string.Empty, rights)); - Assert.Throws (async () => await client.Inbox.RemoveAccessRightsAsync ("smith", null)); - Assert.Throws (async () => await client.Inbox.RemoveAccessRightsAsync ("smith", empty)); + Assert.ThrowsAsync (async () => await client.Inbox.RemoveAccessRightsAsync (null, rights)); + //Assert.ThrowsAsync (async () => await client.Inbox.RemoveAccessRightsAsync (string.Empty, rights)); + Assert.ThrowsAsync (async () => await client.Inbox.RemoveAccessRightsAsync ("smith", null)); + Assert.ThrowsAsync (async () => await client.Inbox.RemoveAccessRightsAsync ("smith", empty)); await client.Inbox.RemoveAccessRightsAsync ("smith", rights); // SETACL INBOX smith lrswida - Assert.Throws (() => client.Inbox.SetAccessRights (null, rights)); - //Assert.Throws (() => client.Inbox.SetAccessRights (string.Empty, rights)); - Assert.Throws (() => client.Inbox.SetAccessRights ("smith", null)); - Assert.Throws (async () => await client.Inbox.SetAccessRightsAsync (null, rights)); - //Assert.Throws (async () => await client.Inbox.SetAccessRightsAsync (string.Empty, rights)); - Assert.Throws (async () => await client.Inbox.SetAccessRightsAsync ("smith", null)); + Assert.ThrowsAsync (async () => await client.Inbox.SetAccessRightsAsync (null, rights)); + //Assert.ThrowsAsync (async () => await client.Inbox.SetAccessRightsAsync (string.Empty, rights)); + Assert.ThrowsAsync (async () => await client.Inbox.SetAccessRightsAsync ("smith", null)); await client.Inbox.SetAccessRightsAsync ("smith", rights); // DELETEACL INBOX smith - Assert.Throws (() => client.Inbox.RemoveAccess (null)); - //Assert.Throws (() => client.Inbox.RemoveAccess (string.Empty)); - Assert.Throws (async () => await client.Inbox.RemoveAccessAsync (null)); - //Assert.Throws (async () => await client.Inbox.RemoveAccessAsync (string.Empty)); + Assert.ThrowsAsync (async () => await client.Inbox.RemoveAccessAsync (null)); + //Assert.ThrowsAsync (async () => await client.Inbox.RemoveAccessAsync (string.Empty)); await client.Inbox.RemoveAccessAsync ("smith"); await client.DisconnectAsync (false); } } + static List CreateMetadataCommands () + { + return new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "metadata.capability.txt"), + new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "metadata.authenticate.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-inbox.txt"), + new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt"), + new ImapReplayCommand ("A00000005 GETMETADATA \"\" /private/comment\r\n", "metadata.getmetadata.txt"), + new ImapReplayCommand ("A00000006 GETMETADATA \"\" (MAXSIZE 1024 DEPTH infinity) (/private)\r\n", "metadata.getmetadata-options.txt"), + new ImapReplayCommand ("A00000007 GETMETADATA \"\" /private/comment /shared/comment\r\n", "metadata.getmetadata-multi.txt"), + new ImapReplayCommand ("A00000008 SETMETADATA \"\" (/private/comment \"this is a comment\")\r\n", "metadata.setmetadata-noprivate.txt"), + new ImapReplayCommand ("A00000009 SETMETADATA \"\" (/private/comment \"this comment is too long!\")\r\n", "metadata.setmetadata-maxsize.txt"), + new ImapReplayCommand ("A00000010 SETMETADATA \"\" (/private/comment \"this is a private comment\" /shared/comment \"this is a shared comment\")\r\n", "metadata.setmetadata-toomany.txt"), + new ImapReplayCommand ("A00000011 SETMETADATA \"\" (/private/comment NIL)\r\n", ImapReplayCommandResponse.OK), + new ImapReplayCommand ("A00000012 GETMETADATA INBOX /private/comment\r\n", "metadata.inbox-getmetadata.txt"), + new ImapReplayCommand ("A00000013 GETMETADATA INBOX (MAXSIZE 1024 DEPTH infinity) (/private)\r\n", "metadata.inbox-getmetadata-options.txt"), + new ImapReplayCommand ("A00000014 GETMETADATA INBOX /private/comment /shared/comment\r\n", "metadata.inbox-getmetadata-multi.txt"), + new ImapReplayCommand ("A00000015 SETMETADATA INBOX (/private/comment \"this is a comment\")\r\n", "metadata.inbox-setmetadata-noprivate.txt"), + new ImapReplayCommand ("A00000016 SETMETADATA INBOX (/private/comment \"this comment is too long!\")\r\n", "metadata.inbox-setmetadata-maxsize.txt"), + new ImapReplayCommand ("A00000017 SETMETADATA INBOX (/private/comment \"this is a private comment\" /shared/comment \"this is a shared comment\")\r\n", "metadata.inbox-setmetadata-toomany.txt"), + new ImapReplayCommand ("A00000018 SETMETADATA INBOX (/private/comment NIL)\r\n", ImapReplayCommandResponse.OK) + }; + } + [Test] - public async void TestMetadata () - { - var commands = new List (); - commands.Add (new ImapReplayCommand ("", "gmail.greeting.txt")); - commands.Add (new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "metadata.capability.txt")); - commands.Add (new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "metadata.authenticate.txt")); - commands.Add (new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt")); - commands.Add (new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\"\r\n", "gmail.list-inbox.txt")); - commands.Add (new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt")); - commands.Add (new ImapReplayCommand ("A00000005 GETMETADATA \"\" /private/comment\r\n", "metadata.getmetadata.txt")); - commands.Add (new ImapReplayCommand ("A00000006 GETMETADATA \"\" (MAXSIZE 1024 DEPTH infinity) (/private)\r\n", "metadata.getmetadata-options.txt")); - commands.Add (new ImapReplayCommand ("A00000007 GETMETADATA \"\" /private/comment /shared/comment\r\n", "metadata.getmetadata-multi.txt")); - commands.Add (new ImapReplayCommand ("A00000008 SETMETADATA \"\" (/private/comment \"this is a comment\")\r\n", "metadata.setmetadata-noprivate.txt")); - commands.Add (new ImapReplayCommand ("A00000009 SETMETADATA \"\" (/private/comment \"this comment is too long!\")\r\n", "metadata.setmetadata-maxsize.txt")); - commands.Add (new ImapReplayCommand ("A00000010 SETMETADATA \"\" (/private/comment \"this is a private comment\" /shared/comment \"this is a shared comment\")\r\n", "metadata.setmetadata-toomany.txt")); - commands.Add (new ImapReplayCommand ("A00000011 SETMETADATA \"\" (/private/comment NIL)\r\n", ImapReplayCommandResponse.OK)); - commands.Add (new ImapReplayCommand ("A00000012 GETMETADATA INBOX /private/comment\r\n", "metadata.inbox-getmetadata.txt")); - commands.Add (new ImapReplayCommand ("A00000013 GETMETADATA INBOX (MAXSIZE 1024 DEPTH infinity) (/private)\r\n", "metadata.inbox-getmetadata-options.txt")); - commands.Add (new ImapReplayCommand ("A00000014 GETMETADATA INBOX /private/comment /shared/comment\r\n", "metadata.inbox-getmetadata-multi.txt")); - commands.Add (new ImapReplayCommand ("A00000015 SETMETADATA INBOX (/private/comment \"this is a comment\")\r\n", "metadata.inbox-setmetadata-noprivate.txt")); - commands.Add (new ImapReplayCommand ("A00000016 SETMETADATA INBOX (/private/comment \"this comment is too long!\")\r\n", "metadata.inbox-setmetadata-maxsize.txt")); - commands.Add (new ImapReplayCommand ("A00000017 SETMETADATA INBOX (/private/comment \"this is a private comment\" /shared/comment \"this is a shared comment\")\r\n", "metadata.inbox-setmetadata-toomany.txt")); - commands.Add (new ImapReplayCommand ("A00000018 SETMETADATA INBOX (/private/comment NIL)\r\n", ImapReplayCommandResponse.OK)); + public void TestMetadata () + { + var commands = CreateMetadataCommands (); - using (var client = new ImapClient ()) { + using (var client = new ImapClient () { TagPrefix = 'A' }) { MetadataCollection metadata; MetadataOptions options; try { - client.ReplayConnect ("localhost", new ImapReplayStream (commands, false)); + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); } catch (Exception ex) { - Assert.Fail ("Did not expect an exception in Connect: {0}", ex); + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); } - Assert.IsTrue (client.IsConnected, "Client failed to connect."); + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); - Assert.AreEqual (MetadataInitialCapabilities, client.Capabilities); - Assert.AreEqual (4, client.AuthenticationMechanisms.Count); - Assert.IsTrue (client.AuthenticationMechanisms.Contains ("XOAUTH"), "Expected SASL XOAUTH auth mechanism"); - Assert.IsTrue (client.AuthenticationMechanisms.Contains ("XOAUTH2"), "Expected SASL XOAUTH2 auth mechanism"); - Assert.IsTrue (client.AuthenticationMechanisms.Contains ("PLAIN"), "Expected SASL PLAIN auth mechanism"); - Assert.IsTrue (client.AuthenticationMechanisms.Contains ("PLAIN-CLIENTTOKEN"), "Expected SASL PLAIN-CLIENTTOKEN auth mechanism"); + Assert.That (client.Capabilities, Is.EqualTo (MetadataInitialCapabilities)); + Assert.That (client.AuthenticationMechanisms, Has.Count.EqualTo (4)); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH"), "Expected SASL XOAUTH auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH2"), "Expected SASL XOAUTH2 auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN"), "Expected SASL PLAIN auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN-CLIENTTOKEN"), "Expected SASL PLAIN-CLIENTTOKEN auth mechanism"); // Note: Do not try XOAUTH2 client.AuthenticationMechanisms.Remove ("XOAUTH2"); try { - await client.AuthenticateAsync ("username", "password"); + client.Authenticate ("username", "password"); } catch (Exception ex) { - Assert.Fail ("Did not expect an exception in Authenticate: {0}", ex); + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); } - Assert.AreEqual (MetadataAuthenticatedCapabilities, client.Capabilities); + Assert.That (client.Capabilities, Is.EqualTo (MetadataAuthenticatedCapabilities)); var inbox = client.Inbox; - Assert.IsNotNull (inbox, "Expected non-null Inbox folder."); - Assert.AreEqual (FolderAttributes.Inbox | FolderAttributes.HasNoChildren, inbox.Attributes, "Expected Inbox attributes to be \\HasNoChildren."); + Assert.That (inbox, Is.Not.Null, "Expected non-null Inbox folder."); + Assert.That (inbox.Attributes, Is.EqualTo (FolderAttributes.Inbox | FolderAttributes.HasNoChildren | FolderAttributes.Subscribed), "Expected Inbox attributes to be \\HasNoChildren."); foreach (var special in Enum.GetValues (typeof (SpecialFolder)).OfType ()) { var folder = client.GetFolder (special); @@ -2260,111 +7351,112 @@ public async void TestMetadata () if (special != SpecialFolder.Archive) { var expected = GetSpecialFolderAttribute (special) | FolderAttributes.HasNoChildren; - Assert.IsNotNull (folder, "Expected non-null {0} folder.", special); - Assert.AreEqual (expected, folder.Attributes, "Expected {0} attributes to be \\HasNoChildren.", special); + Assert.That (folder, Is.Not.Null, $"Expected non-null {special} folder."); + Assert.That (folder.Attributes, Is.EqualTo (expected), $"Expected {special} attributes to be \\HasNoChildren."); } else { - Assert.IsNull (folder, "Expected null {0} folder.", special); + Assert.That (folder, Is.Null, $"Expected null {special} folder."); } } // GETMETADATA - Assert.AreEqual ("this is a comment", await client.GetMetadataAsync (MetadataTag.PrivateComment), "The shared comment does not match."); + Assert.That (client.GetMetadata (MetadataTag.PrivateComment), Is.EqualTo ("this is a comment"), "The shared comment does not match."); options = new MetadataOptions { Depth = int.MaxValue, MaxSize = 1024 }; - metadata = await client.GetMetadataAsync (options, new [] { new MetadataTag ("/private") }); - Assert.AreEqual (1, metadata.Count, "Expected 1 metadata value."); - Assert.AreEqual (MetadataTag.PrivateComment.Id, metadata[0].Tag.Id, "Metadata tag did not match."); - Assert.AreEqual ("this is a private comment", metadata[0].Value, "Metadata value did not match."); - Assert.AreEqual (2199, options.LongEntries, "LongEntries does not match."); - - metadata = await client.GetMetadataAsync (new [] { MetadataTag.PrivateComment, MetadataTag.SharedComment }); - Assert.AreEqual (2, metadata.Count, "Expected 2 metadata values."); - Assert.AreEqual (MetadataTag.PrivateComment.Id, metadata[0].Tag.Id, "First metadata tag did not match."); - Assert.AreEqual (MetadataTag.SharedComment.Id, metadata[1].Tag.Id, "Second metadata tag did not match."); - Assert.AreEqual ("this is a private comment", metadata[0].Value, "First metadata value did not match."); - Assert.AreEqual ("this is a shared comment", metadata[1].Value, "Second metadata value did not match."); + metadata = client.GetMetadata (options, new [] { new MetadataTag ("/private") }); + Assert.That (metadata, Has.Count.EqualTo (1), "Expected 1 metadata value."); + Assert.That (metadata[0].Tag.Id, Is.EqualTo (MetadataTag.PrivateComment.Id), "Metadata tag did not match."); + Assert.That (metadata[0].Value, Is.EqualTo ("this is a private comment"), "Metadata value did not match."); + Assert.That (options.LongEntries, Is.EqualTo (2199), "LongEntries does not match."); + + metadata = client.GetMetadata (new [] { MetadataTag.PrivateComment, MetadataTag.SharedComment }); + Assert.That (metadata, Has.Count.EqualTo (2), "Expected 2 metadata values."); + Assert.That (metadata[0].Tag.Id, Is.EqualTo (MetadataTag.PrivateComment.Id), "First metadata tag did not match."); + Assert.That (metadata[1].Tag.Id, Is.EqualTo (MetadataTag.SharedComment.Id), "Second metadata tag did not match."); + Assert.That (metadata[0].Value, Is.EqualTo ("this is a private comment"), "First metadata value did not match."); + Assert.That (metadata[1].Value, Is.EqualTo ("this is a shared comment"), "Second metadata value did not match."); // SETMETADATA - Assert.Throws (async () => await client.SetMetadataAsync (new MetadataCollection (new [] { + Assert.Throws (() => client.SetMetadata (new MetadataCollection (new [] { new Metadata (MetadataTag.PrivateComment, "this is a comment") })), "Expected NOPRIVATE RESP-CODE."); - Assert.Throws (async () => await client.SetMetadataAsync (new MetadataCollection (new [] { + Assert.Throws (() => client.SetMetadata (new MetadataCollection (new [] { new Metadata (MetadataTag.PrivateComment, "this comment is too long!") })), "Expected MAXSIZE RESP-CODE."); - Assert.Throws (async () => await client.SetMetadataAsync (new MetadataCollection (new [] { + Assert.Throws (() => client.SetMetadata (new MetadataCollection (new [] { new Metadata (MetadataTag.PrivateComment, "this is a private comment"), new Metadata (MetadataTag.SharedComment, "this is a shared comment"), })), "Expected TOOMANY RESP-CODE."); - await client.SetMetadataAsync (new MetadataCollection (new [] { + + // This will no-op + client.SetMetadata (new MetadataCollection ()); + + client.SetMetadata (new MetadataCollection (new [] { new Metadata (MetadataTag.PrivateComment, null) })); // GETMETADATA folder - Assert.AreEqual ("this is a comment", await inbox.GetMetadataAsync (MetadataTag.PrivateComment), "The shared comment does not match."); + Assert.That (inbox.GetMetadata (MetadataTag.PrivateComment), Is.EqualTo ("this is a comment"), "The shared comment does not match."); options = new MetadataOptions { Depth = int.MaxValue, MaxSize = 1024 }; - metadata = await inbox.GetMetadataAsync (options, new [] { new MetadataTag ("/private") }); - Assert.AreEqual (1, metadata.Count, "Expected 1 metadata value."); - Assert.AreEqual (MetadataTag.PrivateComment.Id, metadata[0].Tag.Id, "Metadata tag did not match."); - Assert.AreEqual ("this is a private comment", metadata[0].Value, "Metadata value did not match."); - Assert.AreEqual (2199, options.LongEntries, "LongEntries does not match."); - - metadata = await inbox.GetMetadataAsync (new [] { MetadataTag.PrivateComment, MetadataTag.SharedComment }); - Assert.AreEqual (2, metadata.Count, "Expected 2 metadata values."); - Assert.AreEqual (MetadataTag.PrivateComment.Id, metadata[0].Tag.Id, "First metadata tag did not match."); - Assert.AreEqual (MetadataTag.SharedComment.Id, metadata[1].Tag.Id, "Second metadata tag did not match."); - Assert.AreEqual ("this is a private comment", metadata[0].Value, "First metadata value did not match."); - Assert.AreEqual ("this is a shared comment", metadata[1].Value, "Second metadata value did not match."); + metadata = inbox.GetMetadata (options, new [] { new MetadataTag ("/private") }); + Assert.That (metadata, Has.Count.EqualTo (1), "Expected 1 metadata value."); + Assert.That (metadata[0].Tag.Id, Is.EqualTo (MetadataTag.PrivateComment.Id), "Metadata tag did not match."); + Assert.That (metadata[0].Value, Is.EqualTo ("this is a private comment"), "Metadata value did not match."); + Assert.That (options.LongEntries, Is.EqualTo (2199), "LongEntries does not match."); + + metadata = inbox.GetMetadata (new [] { MetadataTag.PrivateComment, MetadataTag.SharedComment }); + Assert.That (metadata, Has.Count.EqualTo (2), "Expected 2 metadata values."); + Assert.That (metadata[0].Tag.Id, Is.EqualTo (MetadataTag.PrivateComment.Id), "First metadata tag did not match."); + Assert.That (metadata[1].Tag.Id, Is.EqualTo (MetadataTag.SharedComment.Id), "Second metadata tag did not match."); + Assert.That (metadata[0].Value, Is.EqualTo ("this is a private comment"), "First metadata value did not match."); + Assert.That (metadata[1].Value, Is.EqualTo ("this is a shared comment"), "Second metadata value did not match."); + + // This will shortcut and return an empty collection + metadata = client.GetMetadata (Array.Empty ()); + Assert.That (metadata, Is.Empty, "Expected 0 metadata values."); // SETMETADATA folder - Assert.Throws (async () => await inbox.SetMetadataAsync (new MetadataCollection (new [] { + Assert.Throws (() => inbox.SetMetadata (new MetadataCollection (new [] { new Metadata (MetadataTag.PrivateComment, "this is a comment") })), "Expected NOPRIVATE RESP-CODE."); - Assert.Throws (async () => await inbox.SetMetadataAsync (new MetadataCollection (new [] { + Assert.Throws (() => inbox.SetMetadata (new MetadataCollection (new [] { new Metadata (MetadataTag.PrivateComment, "this comment is too long!") })), "Expected MAXSIZE RESP-CODE."); - Assert.Throws (async () => await inbox.SetMetadataAsync (new MetadataCollection (new [] { + Assert.Throws (() => inbox.SetMetadata (new MetadataCollection (new [] { new Metadata (MetadataTag.PrivateComment, "this is a private comment"), new Metadata (MetadataTag.SharedComment, "this is a shared comment"), })), "Expected TOOMANY RESP-CODE."); - await inbox.SetMetadataAsync (new MetadataCollection (new [] { + inbox.SetMetadata (new MetadataCollection (new [] { new Metadata (MetadataTag.PrivateComment, null) })); - await client.DisconnectAsync (false); + client.Disconnect (false); } } [Test] - public async void TestExtractingPrecisePangolinAttachment () + public async Task TestMetadataAsync () { - var commands = new List (); - commands.Add (new ImapReplayCommand ("", "gmail.greeting.txt")); - commands.Add (new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt")); - commands.Add (new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "gmail.authenticate.txt")); - commands.Add (new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt")); - commands.Add (new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\"\r\n", "gmail.list-inbox.txt")); - commands.Add (new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt")); - commands.Add (new ImapReplayCommand ("A00000005 LIST \"\" \"%\"\r\n", "gmail.list-personal.txt")); - commands.Add (new ImapReplayCommand ("A00000006 EXAMINE INBOX (CONDSTORE)\r\n", "gmail.examine-inbox.txt")); - commands.Add (new ImapReplayCommand ("A00000007 FETCH 270 (BODY.PEEK[])\r\n", "gmail.precise-pangolin-message.txt")); + var commands = CreateMetadataCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + MetadataCollection metadata; + MetadataOptions options; - using (var client = new ImapClient ()) { try { - client.ReplayConnect ("localhost", new ImapReplayStream (commands, false)); + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); } catch (Exception ex) { - Assert.Fail ("Did not expect an exception in Connect: {0}", ex); + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); } - Assert.IsTrue (client.IsConnected, "Client failed to connect."); + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); - Assert.AreEqual (GMailInitialCapabilities, client.Capabilities); - Assert.AreEqual (5, client.AuthenticationMechanisms.Count); - Assert.IsTrue (client.AuthenticationMechanisms.Contains ("XOAUTH"), "Expected SASL XOAUTH auth mechanism"); - Assert.IsTrue (client.AuthenticationMechanisms.Contains ("XOAUTH2"), "Expected SASL XOAUTH2 auth mechanism"); - Assert.IsTrue (client.AuthenticationMechanisms.Contains ("OAUTHBEARER"), "Expected SASL OAUTHBEARER auth mechanism"); - Assert.IsTrue (client.AuthenticationMechanisms.Contains ("PLAIN"), "Expected SASL PLAIN auth mechanism"); - Assert.IsTrue (client.AuthenticationMechanisms.Contains ("PLAIN-CLIENTTOKEN"), "Expected SASL PLAIN-CLIENTTOKEN auth mechanism"); + Assert.That (client.Capabilities, Is.EqualTo (MetadataInitialCapabilities)); + Assert.That (client.AuthenticationMechanisms, Has.Count.EqualTo (4)); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH"), "Expected SASL XOAUTH auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH2"), "Expected SASL XOAUTH2 auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN"), "Expected SASL PLAIN auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN-CLIENTTOKEN"), "Expected SASL PLAIN-CLIENTTOKEN auth mechanism"); // Note: Do not try XOAUTH2 client.AuthenticationMechanisms.Remove ("XOAUTH2"); @@ -2372,14 +7464,14 @@ public async void TestExtractingPrecisePangolinAttachment () try { await client.AuthenticateAsync ("username", "password"); } catch (Exception ex) { - Assert.Fail ("Did not expect an exception in Authenticate: {0}", ex); + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); } - Assert.AreEqual (GMailAuthenticatedCapabilities, client.Capabilities); + Assert.That (client.Capabilities, Is.EqualTo (MetadataAuthenticatedCapabilities)); var inbox = client.Inbox; - Assert.IsNotNull (inbox, "Expected non-null Inbox folder."); - Assert.AreEqual (FolderAttributes.Inbox | FolderAttributes.HasNoChildren, inbox.Attributes, "Expected Inbox attributes to be \\HasNoChildren."); + Assert.That (inbox, Is.Not.Null, "Expected non-null Inbox folder."); + Assert.That (inbox.Attributes, Is.EqualTo (FolderAttributes.Inbox | FolderAttributes.HasNoChildren | FolderAttributes.Subscribed), "Expected Inbox attributes to be \\HasNoChildren."); foreach (var special in Enum.GetValues (typeof (SpecialFolder)).OfType ()) { var folder = client.GetFolder (special); @@ -2387,88 +7479,426 @@ public async void TestExtractingPrecisePangolinAttachment () if (special != SpecialFolder.Archive) { var expected = GetSpecialFolderAttribute (special) | FolderAttributes.HasNoChildren; - Assert.IsNotNull (folder, "Expected non-null {0} folder.", special); - Assert.AreEqual (expected, folder.Attributes, "Expected {0} attributes to be \\HasNoChildren.", special); + Assert.That (folder, Is.Not.Null, $"Expected non-null {special} folder."); + Assert.That (folder.Attributes, Is.EqualTo (expected), $"Expected {special} attributes to be \\HasNoChildren."); } else { - Assert.IsNull (folder, "Expected null {0} folder.", special); + Assert.That (folder, Is.Null, $"Expected null {special} folder."); } } - // disable LIST-EXTENDED - client.Capabilities &= ~ImapCapabilities.ListExtended; + // GETMETADATA + Assert.That (await client.GetMetadataAsync (MetadataTag.PrivateComment), Is.EqualTo ("this is a comment"), "The shared comment does not match."); - var personal = client.GetFolder (client.PersonalNamespaces[0]); - var folders = (await personal.GetSubfoldersAsync ()).ToList (); - Assert.AreEqual (client.Inbox, folders[0], "Expected the first folder to be the Inbox."); - Assert.AreEqual ("[Gmail]", folders[1].FullName, "Expected the second folder to be [Gmail]."); - Assert.AreEqual (FolderAttributes.NoSelect | FolderAttributes.HasChildren, folders[1].Attributes, "Expected [Gmail] folder to be \\Noselect \\HasChildren."); + options = new MetadataOptions { Depth = int.MaxValue, MaxSize = 1024 }; + metadata = await client.GetMetadataAsync (options, new [] { new MetadataTag ("/private") }); + Assert.That (metadata, Has.Count.EqualTo (1), "Expected 1 metadata value."); + Assert.That (metadata[0].Tag.Id, Is.EqualTo (MetadataTag.PrivateComment.Id), "Metadata tag did not match."); + Assert.That (metadata[0].Value, Is.EqualTo ("this is a private comment"), "Metadata value did not match."); + Assert.That (options.LongEntries, Is.EqualTo (2199), "LongEntries does not match."); + + metadata = await client.GetMetadataAsync (new [] { MetadataTag.PrivateComment, MetadataTag.SharedComment }); + Assert.That (metadata, Has.Count.EqualTo (2), "Expected 2 metadata values."); + Assert.That (metadata[0].Tag.Id, Is.EqualTo (MetadataTag.PrivateComment.Id), "First metadata tag did not match."); + Assert.That (metadata[1].Tag.Id, Is.EqualTo (MetadataTag.SharedComment.Id), "Second metadata tag did not match."); + Assert.That (metadata[0].Value, Is.EqualTo ("this is a private comment"), "First metadata value did not match."); + Assert.That (metadata[1].Value, Is.EqualTo ("this is a shared comment"), "Second metadata value did not match."); + + // This will shortcut and return an empty collection + metadata = await client.GetMetadataAsync (Array.Empty ()); + Assert.That (metadata, Is.Empty, "Expected 0 metadata values."); + + // SETMETADATA + Assert.ThrowsAsync (async () => await client.SetMetadataAsync (new MetadataCollection (new [] { + new Metadata (MetadataTag.PrivateComment, "this is a comment") + })), "Expected NOPRIVATE RESP-CODE."); + Assert.ThrowsAsync (async () => await client.SetMetadataAsync (new MetadataCollection (new [] { + new Metadata (MetadataTag.PrivateComment, "this comment is too long!") + })), "Expected MAXSIZE RESP-CODE."); + Assert.ThrowsAsync (async () => await client.SetMetadataAsync (new MetadataCollection (new [] { + new Metadata (MetadataTag.PrivateComment, "this is a private comment"), + new Metadata (MetadataTag.SharedComment, "this is a shared comment"), + })), "Expected TOOMANY RESP-CODE."); - await client.Inbox.OpenAsync (FolderAccess.ReadOnly); + // This will no-op + await client.SetMetadataAsync (new MetadataCollection ()); - var message = await client.Inbox.GetMessageAsync (269); + await client.SetMetadataAsync (new MetadataCollection (new [] { + new Metadata (MetadataTag.PrivateComment, null) + })); - using (var jpeg = new MemoryStream ()) { - var attachment = message.Attachments.OfType ().FirstOrDefault (); + // GETMETADATA folder + Assert.That (await inbox.GetMetadataAsync (MetadataTag.PrivateComment), Is.EqualTo ("this is a comment"), "The shared comment does not match."); - attachment.ContentObject.DecodeTo (jpeg); - jpeg.Position = 0; + options = new MetadataOptions { Depth = int.MaxValue, MaxSize = 1024 }; + metadata = await inbox.GetMetadataAsync (options, new [] { new MetadataTag ("/private") }); + Assert.That (metadata, Has.Count.EqualTo (1), "Expected 1 metadata value."); + Assert.That (metadata[0].Tag.Id, Is.EqualTo (MetadataTag.PrivateComment.Id), "Metadata tag did not match."); + Assert.That (metadata[0].Value, Is.EqualTo ("this is a private comment"), "Metadata value did not match."); + Assert.That (options.LongEntries, Is.EqualTo (2199), "LongEntries does not match."); - using (var md5 = new MD5CryptoServiceProvider ()) { - var md5sum = HexEncode (md5.ComputeHash (jpeg)); + metadata = await inbox.GetMetadataAsync (new [] { MetadataTag.PrivateComment, MetadataTag.SharedComment }); + Assert.That (metadata, Has.Count.EqualTo (2), "Expected 2 metadata values."); + Assert.That (metadata[0].Tag.Id, Is.EqualTo (MetadataTag.PrivateComment.Id), "First metadata tag did not match."); + Assert.That (metadata[1].Tag.Id, Is.EqualTo (MetadataTag.SharedComment.Id), "Second metadata tag did not match."); + Assert.That (metadata[0].Value, Is.EqualTo ("this is a private comment"), "First metadata value did not match."); + Assert.That (metadata[1].Value, Is.EqualTo ("this is a shared comment"), "Second metadata value did not match."); - Assert.AreEqual ("167a46aa81e881da2ea8a840727384d3", md5sum, "MD5 checksums do not match."); - } - } + // SETMETADATA folder + Assert.ThrowsAsync (async () => await inbox.SetMetadataAsync (new MetadataCollection (new [] { + new Metadata (MetadataTag.PrivateComment, "this is a comment") + })), "Expected NOPRIVATE RESP-CODE."); + Assert.ThrowsAsync (async () => await inbox.SetMetadataAsync (new MetadataCollection (new [] { + new Metadata (MetadataTag.PrivateComment, "this comment is too long!") + })), "Expected MAXSIZE RESP-CODE."); + Assert.ThrowsAsync (async () => await inbox.SetMetadataAsync (new MetadataCollection (new [] { + new Metadata (MetadataTag.PrivateComment, "this is a private comment"), + new Metadata (MetadataTag.SharedComment, "this is a shared comment"), + })), "Expected TOOMANY RESP-CODE."); + await inbox.SetMetadataAsync (new MetadataCollection (new [] { + new Metadata (MetadataTag.PrivateComment, null) + })); await client.DisconnectAsync (false); } } - + + static List CreateNamespaceExtensionCommands () + { + return new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt"), + new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "gmail.authenticate.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "common.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-inbox.txt"), + new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt"), + new ImapReplayCommand ("A00000005 LOGOUT\r\n", "gmail.logout.txt") + }; + } + [Test] - public async void TestMessageCount () + public void TestNamespaceExtensions () { - var commands = new List (); - commands.Add (new ImapReplayCommand ("", "gmail.greeting.txt")); - commands.Add (new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt")); - commands.Add (new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "gmail.authenticate.txt")); - commands.Add (new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt")); - commands.Add (new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\"\r\n", "gmail.list-inbox.txt")); - commands.Add (new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt")); - // INBOX has 1 message present in this test - commands.Add (new ImapReplayCommand ("A00000005 EXAMINE INBOX (CONDSTORE)\r\n", "gmail.count.examine.txt")); - // next command simulates one expunge + one new message - commands.Add (new ImapReplayCommand ("A00000006 NOOP\r\n", "gmail.count.noop.txt")); + var commands = CreateNamespaceExtensionCommands (); - using (var client = new ImapClient ()) { + using (var client = new ImapClient () { TagPrefix = 'A' }) { try { - client.ReplayConnect ("localhost", new ImapReplayStream (commands, false)); + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); } catch (Exception ex) { - Assert.Fail ("Did not expect an exception in Connect: {0}", ex); + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); } - Assert.IsTrue (client.IsConnected, "Client failed to connect."); - - // Note: Do not try XOAUTH2 - client.AuthenticationMechanisms.Remove ("XOAUTH2"); + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.PersonalNamespaces, Has.Count.EqualTo (1), "PersonalNamespaces.Count"); + Assert.That (client.PersonalNamespaces[0].Path, Is.EqualTo (string.Empty), "PersonalNamespaces[0].Path"); + Assert.That (client.PersonalNamespaces[0].DirectorySeparator, Is.EqualTo ('/'), "PersonalNamespaces[0].DirectorySeparator"); + + Assert.That (client.OtherNamespaces, Has.Count.EqualTo (1), "OtherNamespaces.Count"); + Assert.That (client.OtherNamespaces[0].Path, Is.EqualTo ("Other Users"), "OtherNamespaces[0].Path"); + Assert.That (client.OtherNamespaces[0].DirectorySeparator, Is.EqualTo ('/'), "OtherNamespaces[0].DirectorySeparator"); + + Assert.That (client.SharedNamespaces, Has.Count.EqualTo (1), "SharedNamespaces.Count"); + Assert.That (client.SharedNamespaces[0].Path, Is.EqualTo ("Public Folders"), "SharedNamespaces[0].Path"); + Assert.That (client.SharedNamespaces[0].DirectorySeparator, Is.EqualTo ('/'), "SharedNamespaces[0].DirectorySeparator"); + + client.Disconnect (true); + } + } + + [Test] + public async Task TestNamespaceExtensionsAsync () + { + var commands = CreateNamespaceExtensionCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); try { await client.AuthenticateAsync ("username", "password"); } catch (Exception ex) { - Assert.Fail ("Did not expect an exception in Authenticate: {0}", ex); + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); } - - var count = -1; - - await client.Inbox.OpenAsync (FolderAccess.ReadOnly); - - client.Inbox.CountChanged += delegate { - count = client.Inbox.Count; - }; - - await client.NoOpAsync (); - - Assert.AreEqual (1, count, "Count is not correct"); + + Assert.That (client.PersonalNamespaces, Has.Count.EqualTo (1), "PersonalNamespaces.Count"); + Assert.That (client.PersonalNamespaces[0].Path, Is.EqualTo (string.Empty), "PersonalNamespaces[0].Path"); + Assert.That (client.PersonalNamespaces[0].DirectorySeparator, Is.EqualTo ('/'), "PersonalNamespaces[0].DirectorySeparator"); + + Assert.That (client.OtherNamespaces, Has.Count.EqualTo (1), "OtherNamespaces.Count"); + Assert.That (client.OtherNamespaces[0].Path, Is.EqualTo ("Other Users"), "OtherNamespaces[0].Path"); + Assert.That (client.OtherNamespaces[0].DirectorySeparator, Is.EqualTo ('/'), "OtherNamespaces[0].DirectorySeparator"); + + Assert.That (client.SharedNamespaces, Has.Count.EqualTo (1), "SharedNamespaces.Count"); + Assert.That (client.SharedNamespaces[0].Path, Is.EqualTo ("Public Folders"), "SharedNamespaces[0].Path"); + Assert.That (client.SharedNamespaces[0].DirectorySeparator, Is.EqualTo ('/'), "SharedNamespaces[0].DirectorySeparator"); + + await client.DisconnectAsync (true); + } + } + + static List CreateListInboxFallbackAfterEmptyListExtendedCommands () + { + return new List { + new ImapReplayCommand ("", "strato.de.greeting.txt"), + new ImapReplayCommand ("A00000000 AUTHENTICATE PLAIN\r\n", ImapReplayCommandResponse.Plus), + new ImapReplayCommand ("A00000000", "AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "strato.de.authenticate.txt"), + new ImapReplayCommand ("A00000001 CAPABILITY\r\n", "strato.de.capability.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "strato.de.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", ImapReplayCommandResponse.OK), + new ImapReplayCommand ("A00000004 LIST \"\" \"INBOX\"\r\n", "strato.de.list-inbox.txt"), + new ImapReplayCommand ("A00000005 XLIST \"\" \"*\"\r\n", "strato.de.xlist.txt"), + new ImapReplayCommand ("A00000006 LOGOUT\r\n", ImapReplayCommandResponse.OK) + }; + } + + [Test] + public void TestListInboxFallbackAfterEmptyListExtended () + { + const ImapCapabilities InitialCapabilities = ImapCapabilities.IMAP4 | ImapCapabilities.IMAP4rev1 | ImapCapabilities.Status | + ImapCapabilities.AppendLimit | ImapCapabilities.Enable | ImapCapabilities.Id | ImapCapabilities.Idle | ImapCapabilities.Move | + ImapCapabilities.ListExtended | ImapCapabilities.Namespace | ImapCapabilities.Quota | ImapCapabilities.Sort | + ImapCapabilities.SpecialUse | ImapCapabilities.UidPlus; + const ImapCapabilities AuthenticatedCapabilities = ImapCapabilities.IMAP4 | ImapCapabilities.IMAP4rev1 | ImapCapabilities.Status | + ImapCapabilities.AppendLimit | ImapCapabilities.CreateSpecialUse | ImapCapabilities.Quota | ImapCapabilities.Children | + ImapCapabilities.CondStore | ImapCapabilities.Enable | ImapCapabilities.ESort | ImapCapabilities.ESearch | ImapCapabilities.I18NLevel | + ImapCapabilities.Id | ImapCapabilities.Idle | ImapCapabilities.Move | /*ImapCapabilities.ListStatus | ImapCapabilities.ListExtended |*/ + ImapCapabilities.LiteralPlus | ImapCapabilities.Namespace | /*ImapCapabilities.Preview |*/ ImapCapabilities.FuzzySearch | + ImapCapabilities.Sort | ImapCapabilities.SearchResults | /*ImapCapabilities.SpecialUse |*/ ImapCapabilities.StatusSize | + ImapCapabilities.UidPlus | ImapCapabilities.Unselect | ImapCapabilities.Within | ImapCapabilities.XList; + var commands = CreateListInboxFallbackAfterEmptyListExtendedCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + + Assert.That (client.Capabilities, Is.EqualTo (InitialCapabilities)); + Assert.That (client.AppendLimit, Is.EqualTo (104857600), "AppendLimit"); + Assert.That (client.AuthenticationMechanisms, Has.Count.EqualTo (2)); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN"), "Expected SASL PLAIN auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("LOGIN"), "Expected SASL LOGIN auth mechanism"); + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities, Is.EqualTo (AuthenticatedCapabilities)); + Assert.That (client.PersonalNamespaces, Has.Count.EqualTo (1), "PersonalNamespaces.Count"); + Assert.That (client.PersonalNamespaces[0].Path, Is.EqualTo (string.Empty), "PersonalNamespaces[0].Path"); + Assert.That (client.PersonalNamespaces[0].DirectorySeparator, Is.EqualTo ('.'), "PersonalNamespaces[0].DirectorySeparator"); + Assert.That (client.OtherNamespaces, Has.Count.EqualTo (0), "OtherNamespaces.Count"); + Assert.That (client.SharedNamespaces, Has.Count.EqualTo (0), "SharedNamespaces.Count"); + + Assert.That (client.Inbox, Is.Not.Null, "Inbox"); + + client.Disconnect (true); + } + } + + [Test] + public async Task TestListInboxFallbackAfterEmptyListExtendedAsync () + { + const ImapCapabilities InitialCapabilities = ImapCapabilities.IMAP4 | ImapCapabilities.IMAP4rev1 | ImapCapabilities.Status | + ImapCapabilities.AppendLimit | ImapCapabilities.Enable | ImapCapabilities.Id | ImapCapabilities.Idle | ImapCapabilities.Move | + ImapCapabilities.ListExtended | ImapCapabilities.Namespace | ImapCapabilities.Quota | ImapCapabilities.Sort | + ImapCapabilities.SpecialUse | ImapCapabilities.UidPlus; + const ImapCapabilities AuthenticatedCapabilities = ImapCapabilities.IMAP4 | ImapCapabilities.IMAP4rev1 | ImapCapabilities.Status | + ImapCapabilities.AppendLimit | ImapCapabilities.CreateSpecialUse | ImapCapabilities.Quota | ImapCapabilities.Children | + ImapCapabilities.CondStore | ImapCapabilities.Enable | ImapCapabilities.ESort | ImapCapabilities.ESearch | ImapCapabilities.I18NLevel | + ImapCapabilities.Id | ImapCapabilities.Idle | ImapCapabilities.Move | /*ImapCapabilities.ListStatus | ImapCapabilities.ListExtended |*/ + ImapCapabilities.LiteralPlus | ImapCapabilities.Namespace | /*ImapCapabilities.Preview |*/ ImapCapabilities.FuzzySearch | + ImapCapabilities.Sort | ImapCapabilities.SearchResults | /*ImapCapabilities.SpecialUse |*/ ImapCapabilities.StatusSize | + ImapCapabilities.UidPlus | ImapCapabilities.Unselect | ImapCapabilities.Within | ImapCapabilities.XList; + var commands = CreateListInboxFallbackAfterEmptyListExtendedCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + + Assert.That (client.Capabilities, Is.EqualTo (InitialCapabilities)); + Assert.That (client.AppendLimit, Is.EqualTo (104857600), "AppendLimit"); + Assert.That (client.AuthenticationMechanisms, Has.Count.EqualTo (2)); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN"), "Expected SASL PLAIN auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("LOGIN"), "Expected SASL LOGIN auth mechanism"); + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities, Is.EqualTo (AuthenticatedCapabilities)); + Assert.That (client.PersonalNamespaces, Has.Count.EqualTo (1), "PersonalNamespaces.Count"); + Assert.That (client.PersonalNamespaces[0].Path, Is.EqualTo (string.Empty), "PersonalNamespaces[0].Path"); + Assert.That (client.PersonalNamespaces[0].DirectorySeparator, Is.EqualTo ('.'), "PersonalNamespaces[0].DirectorySeparator"); + Assert.That (client.OtherNamespaces, Has.Count.EqualTo (0), "OtherNamespaces.Count"); + Assert.That (client.SharedNamespaces, Has.Count.EqualTo (0), "SharedNamespaces.Count"); + + Assert.That (client.Inbox, Is.Not.Null, "Inbox"); + + await client.DisconnectAsync (true); + } + } + + [Test] + public void TestLowercaseImapResponses () + { + var commands = new List { + new ImapReplayCommand ("", "lowercase.greeting.txt"), + new ImapReplayCommand ("A00000000 LOGIN username password\r\n", ImapReplayCommandResponse.OK), + new ImapReplayCommand ("A00000001 CAPABILITY\r\n", "lowercase.capability.txt"), + new ImapReplayCommand ("A00000002 LIST \"\" \"\"\r\n", "lowercase.list.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\"\r\n", "lowercase.list.txt"), + new ImapReplayCommand ("A00000004 LIST (SPECIAL-USE) \"\" \"*\"\r\n", "lowercase.list.txt") + }; + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.PersonalNamespaces, Has.Count.EqualTo (1), "PersonalNamespaces.Count"); + Assert.That (client.PersonalNamespaces[0].Path, Is.EqualTo (string.Empty), "PersonalNamespaces[0].Path"); + Assert.That (client.PersonalNamespaces[0].DirectorySeparator, Is.EqualTo ('/'), "PersonalNamespaces[0].DirectorySeparator"); + Assert.That (client.OtherNamespaces, Is.Empty, "OtherNamespaces.Count"); + Assert.That (client.SharedNamespaces, Is.Empty, "SharedNamespaces.Count"); + + Assert.That (client.Inbox, Is.Not.Null, "Inbox"); } } + + static void TestQuirksModeDetectionBasedOnGreeting (string greeting, string capability, ImapQuirksMode quirksMode) + { + var commands = new List { + new ImapReplayCommand ("", greeting), + }; + + if (capability != null) + commands.Add (new ImapReplayCommand ("A00000000 CAPABILITY\r\n", capability)); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + + var engine = (ImapEngine) client.SyncRoot; + + Assert.That (engine.QuirksMode, Is.EqualTo (quirksMode), "QuirksMode"); + } + } + + [Test] + public void TestQuirksModeDetectionCourier () + { + TestQuirksModeDetectionBasedOnGreeting ("courier.greeting.txt", null, ImapQuirksMode.Courier); + } + + [Test] + public void TestQuirksModeDetectionCyrus () + { + TestQuirksModeDetectionBasedOnGreeting ("cyrus.greeting.txt", null, ImapQuirksMode.Cyrus); + } + + [Test] + public void TestQuirksModeDetectionDomino () + { + TestQuirksModeDetectionBasedOnGreeting ("domino.greeting.txt", "domino.capability.txt", ImapQuirksMode.Domino); + } + + [Test] + public void TestQuirksModeDetectionDovecot () + { + TestQuirksModeDetectionBasedOnGreeting ("dovecot.greeting.txt", null, ImapQuirksMode.Dovecot); + } + + [Test] + public void TestQuirksModeDetectionExchange2003 () + { + TestQuirksModeDetectionBasedOnGreeting ("exchange.greeting-2003.txt", "exchange.capability-preauth.txt", ImapQuirksMode.Exchange2003); + } + + [Test] + public void TestQuirksModeDetectionExchange2007 () + { + TestQuirksModeDetectionBasedOnGreeting ("exchange.greeting-2007.txt", "exchange.capability-preauth.txt", ImapQuirksMode.Exchange2007); + } + + [Test] + public void TestQuirksModeDetectionGMail () + { + TestQuirksModeDetectionBasedOnGreeting ("gmail.greeting.txt", "gmail.capability.txt", ImapQuirksMode.GMail); + } + + [Test] + public void TestQuirksModeDetectionQQMail () + { + TestQuirksModeDetectionBasedOnGreeting ("qqmail.greeting.txt", null, ImapQuirksMode.QQMail); + } + + [Test] + public void TestQuirksModeDetectionSmarterMail () + { + TestQuirksModeDetectionBasedOnGreeting ("smartermail.greeting.txt", "common.capability.txt", ImapQuirksMode.SmarterMail); + } + + [Test] + public void TestQuirksModeDetectionUW () + { + TestQuirksModeDetectionBasedOnGreeting ("uw.greeting.txt", null, ImapQuirksMode.UW); + } + + [Test] + public void TestQuirksModeDetectionYahooMail () + { + TestQuirksModeDetectionBasedOnGreeting ("yahoo.greeting.txt", "yahoo.capabilities.txt", ImapQuirksMode.Yahoo); + } + + [Test] + public void TestQuirksModeDetectionYandex () + { + TestQuirksModeDetectionBasedOnGreeting ("yandex.greeting.txt", "yandex.capability.txt", ImapQuirksMode.Yandex); + } + + [Test] + public void TestQuirksModeDetectionZoho () + { + TestQuirksModeDetectionBasedOnGreeting ("zoho.greeting.txt", "zoho.capability.txt", ImapQuirksMode.Zoho); + } } } diff --git a/UnitTests/Net/Imap/ImapCommandExceptionTests.cs b/UnitTests/Net/Imap/ImapCommandExceptionTests.cs new file mode 100644 index 0000000000..ab8085ee2d --- /dev/null +++ b/UnitTests/Net/Imap/ImapCommandExceptionTests.cs @@ -0,0 +1,79 @@ +// +// +// ImapCommandExceptionTests.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +#if NET6_0 + +using System.Runtime.Serialization.Formatters.Binary; + +using MailKit.Net.Imap; + +namespace UnitTests.Net.Imap { + [TestFixture] + public class ImapCommandExceptionTests + { + [Test] + public void TestImapCommandException () + { + ImapCommandException expected; + + expected = new ImapCommandException (ImapCommandResponse.Ok, "This is the response text."); + using (var stream = new MemoryStream ()) { + var formatter = new BinaryFormatter (); + formatter.Serialize (stream, expected); + stream.Position = 0; + + var ex = (ImapCommandException) formatter.Deserialize (stream); + Assert.That (ex.Response, Is.EqualTo (expected.Response), "Unexpected Response."); + Assert.That (ex.ResponseText, Is.EqualTo (expected.ResponseText), "Unexpected ResponseText."); + } + + expected = new ImapCommandException (ImapCommandResponse.Ok, "This is the response text.", "This is the error message."); + using (var stream = new MemoryStream ()) { + var formatter = new BinaryFormatter (); + formatter.Serialize (stream, expected); + stream.Position = 0; + + var ex = (ImapCommandException) formatter.Deserialize (stream); + Assert.That (ex.Response, Is.EqualTo (expected.Response), "Unexpected Response."); + Assert.That (ex.ResponseText, Is.EqualTo (expected.ResponseText), "Unexpected ResponseText."); + } + + expected = new ImapCommandException (ImapCommandResponse.Ok, "This is the response text.", "This is the error message.", new IOException ("This is the IO error.")); + using (var stream = new MemoryStream ()) { + var formatter = new BinaryFormatter (); + formatter.Serialize (stream, expected); + stream.Position = 0; + + var ex = (ImapCommandException) formatter.Deserialize (stream); + Assert.That (ex.Response, Is.EqualTo (expected.Response), "Unexpected Response."); + Assert.That (ex.ResponseText, Is.EqualTo (expected.ResponseText), "Unexpected ResponseText."); + } + } + } +} + +#endif // NET6_0 diff --git a/UnitTests/Net/Imap/ImapCommandTests.cs b/UnitTests/Net/Imap/ImapCommandTests.cs new file mode 100644 index 0000000000..cd9364c956 --- /dev/null +++ b/UnitTests/Net/Imap/ImapCommandTests.cs @@ -0,0 +1,140 @@ +// +// ImapCommandTests.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System.Text; + +using MimeKit; +using MailKit; +using MailKit.Net.Imap; + +namespace UnitTests.Net.Imap { + [TestFixture] + public class ImapCommandTests : IDisposable + { + readonly ImapEngine Engine; + readonly ImapFolder Inbox; + + public ImapCommandTests () + { + Engine = new ImapEngine (CreateImapFolderDelegate) { + Capabilities = ImapCapabilities.IMAP4rev1 + }; + + var args = new ImapFolderConstructorArgs (Engine, "INBOX", FolderAttributes.None, '.'); + Inbox = new ImapFolder (args); + } + + public void Dispose () + { + Engine.Dispose (); + GC.SuppressFinalize (this); + } + + static ImapFolder CreateImapFolderDelegate (ImapFolderConstructorArgs args) + { + return new ImapFolder (args); + } + + static Task UntaggedResponseHandler (ImapEngine engine, ImapCommand ic, int index, bool doAsync) + { + return Task.CompletedTask; + } + + [Test] + public void TestArgumentExceptions () + { + Assert.Throws (() => new ImapCommand (null, CancellationToken.None, Inbox, "NOOP\r\n")); + Assert.Throws (() => new ImapCommand (Engine, CancellationToken.None, Inbox, null)); + + Assert.Throws (() => new ImapCommand (null, CancellationToken.None, Inbox, FormatOptions.Default, "NOOP\r\n")); + Assert.Throws (() => new ImapCommand (Engine, CancellationToken.None, Inbox, null, "NOOP\r\n")); + Assert.Throws (() => new ImapCommand (Engine, CancellationToken.None, Inbox, FormatOptions.Default, null)); + + var ic = new ImapCommand (Engine, CancellationToken.None, Inbox, "NOOP\r\n"); + Assert.Throws (() => ic.RegisterUntaggedHandler (null, UntaggedResponseHandler)); + Assert.Throws (() => ic.RegisterUntaggedHandler ("EVENT", null)); + + ic.Status = ImapCommandStatus.Queued; + Assert.Throws (() => ic.RegisterUntaggedHandler ("EVENT", UntaggedResponseHandler)); + + ic.Status = ImapCommandStatus.Active; + Assert.Throws (() => ic.RegisterUntaggedHandler ("EVENT", UntaggedResponseHandler)); + + ic.Status = ImapCommandStatus.Complete; + Assert.Throws (() => ic.RegisterUntaggedHandler ("EVENT", UntaggedResponseHandler)); + + ic.Status = ImapCommandStatus.Error; + Assert.Throws (() => ic.RegisterUntaggedHandler ("EVENT", UntaggedResponseHandler)); + } + + [Test] + public void TestFormatExceptions () + { + try { + var ic = new ImapCommand (Engine, CancellationToken.None, null, "Lets try %X as a format argument."); + Assert.Fail ("Expected FormatException"); + } catch (FormatException ex) { + Assert.That (ex.Message, Is.EqualTo ("The %X format specifier is not supported.")); + } catch (Exception ex) { + Assert.Fail ($"Expected FormatException, but got {ex.GetType ().Name}"); + } + + try { + var ic = ImapCommand.EstimateCommandLength (Engine, "Lets try %Y as a format argument."); + Assert.Fail ("Expected FormatException"); + } catch (FormatException ex) { + Assert.That (ex.Message, Is.EqualTo ("The %Y format specifier is not supported.")); + } catch (Exception ex) { + Assert.Fail ($"Expected FormatException, but got {ex.GetType ().Name}"); + } + } + + [Test] + public void TestEstimateCommandLengthWithLiteralString () + { + const string koreanProverb = "꿩 먹고 알 먹는다"; + var literalLength = Encoding.UTF8.GetByteCount (koreanProverb); + var expected = $"SEARCH TEXT {{{literalLength}}}\r\n{koreanProverb}".Length; + + var length = ImapCommand.EstimateCommandLength (Engine, "SEARCH TEXT %S", koreanProverb); + Assert.That (length, Is.EqualTo (expected)); + + try { + Engine.Capabilities = ImapCapabilities.IMAP4rev1 | ImapCapabilities.LiteralPlus; + expected = $"SEARCH TEXT {{{literalLength}+}}\r\n{koreanProverb}".Length; + length = ImapCommand.EstimateCommandLength (Engine, "SEARCH TEXT %S", koreanProverb); + Assert.That (length, Is.EqualTo (expected), "LITERAL+"); + + Engine.Capabilities = ImapCapabilities.IMAP4rev1 | ImapCapabilities.LiteralMinus; + expected = $"SEARCH TEXT {{{literalLength}+}}\r\n{koreanProverb}".Length; + length = ImapCommand.EstimateCommandLength (Engine, "SEARCH TEXT %S", koreanProverb); + Assert.That (length, Is.EqualTo (expected), "LITERAL-"); + } finally { + Engine.Capabilities = ImapCapabilities.IMAP4rev1; + } + } + } +} diff --git a/UnitTests/Net/Imap/ImapEncodingTests.cs b/UnitTests/Net/Imap/ImapEncodingTests.cs index c199d0be1a..69e59230c6 100644 --- a/UnitTests/Net/Imap/ImapEncodingTests.cs +++ b/UnitTests/Net/Imap/ImapEncodingTests.cs @@ -1,9 +1,9 @@ -// +// // ImapEncodingTests.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -24,26 +24,34 @@ // THE SOFTWARE. // -using System; - -using NUnit.Framework; - using MailKit.Net.Imap; namespace UnitTests.Net.Imap { [TestFixture] public class ImapEncodingTests { + [Test] + public void TestAmpersand () + { + const string text = "Jack & Jill"; + + var encoded = ImapEncoding.Encode (text); + Assert.That (encoded, Is.EqualTo ("Jack &- Jill"), $"UTF-7 encoded text does not match the expected value: {encoded}"); + + var decoded = ImapEncoding.Decode (encoded); + Assert.That (decoded, Is.EqualTo (text), $"UTF-7 decoded text does not match the original text: {decoded}"); + } + [Test] public void TestArabicExample () { const string arabic = "هل تتكلم اللغة الإنجليزية /العربية؟"; var encoded = ImapEncoding.Encode (arabic); - Assert.AreEqual ("&BkcGRA- &BioGKgZDBkQGRQ- &BicGRAZEBjoGKQ- &BicGRAYlBkYGLAZEBkoGMgZKBik- /&BicGRAY5BjEGKAZKBikGHw-", encoded, "UTF-7 encoded text does not match the expected value: {0}", encoded); + Assert.That (encoded, Is.EqualTo ("&BkcGRA- &BioGKgZDBkQGRQ- &BicGRAZEBjoGKQ- &BicGRAYlBkYGLAZEBkoGMgZKBik- /&BicGRAY5BjEGKAZKBikGHw-"), $"UTF-7 encoded text does not match the expected value: {encoded}"); var decoded = ImapEncoding.Decode (encoded); - Assert.AreEqual (arabic, decoded, "UTF-7 decoded text does not match the original text: {0}", decoded); + Assert.That (decoded, Is.EqualTo (arabic), $"UTF-7 decoded text does not match the original text: {decoded}"); } [Test] @@ -52,10 +60,10 @@ public void TestJapaneseExample () const string japanese = "狂ったこの世で狂うなら気は確かだ。"; var encoded = ImapEncoding.Encode (japanese); - Assert.AreEqual ("&csIwYzBfMFMwbk4WMGdywjBGMGowiWwXMG94ujBLMGAwAg-", encoded, "UTF-7 encoded text does not match the expected value: {0}", encoded); + Assert.That (encoded, Is.EqualTo ("&csIwYzBfMFMwbk4WMGdywjBGMGowiWwXMG94ujBLMGAwAg-"), $"UTF-7 encoded text does not match the expected value: {encoded}"); var decoded = ImapEncoding.Decode (encoded); - Assert.AreEqual (japanese, decoded, "UTF-7 decoded text does not match the original text: {0}", decoded); + Assert.That (decoded, Is.EqualTo (japanese), $"UTF-7 decoded text does not match the original text: {decoded}"); } [Test] @@ -66,10 +74,10 @@ public void TestSurrogatePairs () var text = "Les Mise" + char.ConvertFromUtf32 (0x301) + "rables"; var encoded = ImapEncoding.Encode (text); - Assert.AreEqual ("Les Mise&AwE-rables", encoded, "UTF-7 encoded text does not match the expected value: {0}", encoded); + Assert.That (encoded, Is.EqualTo ("Les Mise&AwE-rables"), $"UTF-7 encoded text does not match the expected value: {encoded}"); var decoded = ImapEncoding.Decode (encoded); - Assert.AreEqual (text, decoded, "UTF-7 decoded text does not match the original text: {0}", decoded); + Assert.That (decoded, Is.EqualTo (text), $"UTF-7 decoded text does not match the original text: {decoded}"); } [Test] @@ -78,10 +86,10 @@ public void TestChineseSurrogatePairs () const string chinese = "‎中國哲學書電子化計劃"; var encoded = ImapEncoding.Encode (chinese); - Assert.AreEqual ("&IA5OLVcLVPJbeGb4lvtbUFMWighSgw-", encoded, "UTF-7 encoded text does not match the expected value: {0}", encoded); + Assert.That (encoded, Is.EqualTo ("&IA5OLVcLVPJbeGb4lvtbUFMWighSgw-"), $"UTF-7 encoded text does not match the expected value: {encoded}"); var decoded = ImapEncoding.Decode (encoded); - Assert.AreEqual (chinese, decoded, "UTF-7 decoded text does not match the original text: {0}", decoded); + Assert.That (decoded, Is.EqualTo (chinese), $"UTF-7 decoded text does not match the original text: {decoded}"); } [Test] @@ -90,10 +98,10 @@ public void TestRfc3501Example () const string mixed = "~peter/mail/台北/日本語"; var encoded = ImapEncoding.Encode (mixed); - Assert.AreEqual ("~peter/mail/&U,BTFw-/&ZeVnLIqe-", encoded, "UTF-7 encoded text does not match the expected value: {0}", encoded); + Assert.That (encoded, Is.EqualTo ("~peter/mail/&U,BTFw-/&ZeVnLIqe-"), $"UTF-7 encoded text does not match the expected value: {encoded}"); var decoded = ImapEncoding.Decode (encoded); - Assert.AreEqual (mixed, decoded, "UTF-7 decoded text does not match the original text: {0}", decoded); + Assert.That (decoded, Is.EqualTo (mixed), $"UTF-7 decoded text does not match the original text: {decoded}"); } [Test] @@ -102,7 +110,16 @@ public void TestDecodeBadRfc3501Example () const string encoded = "&Jjo!"; var decoded = ImapEncoding.Decode (encoded); - Assert.AreEqual (encoded, decoded, "UTF-7 decoded text does not match the original text: {0}", decoded); + Assert.That (decoded, Is.EqualTo (encoded), $"UTF-7 decoded text does not match the original text: {decoded}"); + } + + [Test] + public void TestDecodeInvalidUtf7 () + { + const string encoded = "&台北日本語"; + + var decoded = ImapEncoding.Decode (encoded); + Assert.That (decoded, Is.EqualTo (encoded), $"UTF-7 decoded text does not match the original text: {decoded}"); } [Test] @@ -112,10 +129,10 @@ public void TestRfc3501SuperfluousShiftExample () // Note: we may want to modify ImapEncoding.Decode() to fail and return the input text in this case var decoded = ImapEncoding.Decode (example); - Assert.AreEqual ("台北日本語", decoded, "UTF-7 decoded text does not match the expected value."); + Assert.That (decoded, Is.EqualTo ("台北日本語"), "UTF-7 decoded text does not match the expected value."); var encoded = ImapEncoding.Encode (decoded); - Assert.AreEqual ("&U,BTF2XlZyyKng-", encoded, "UTF-7 encoded text does not match the expected value."); + Assert.That (encoded, Is.EqualTo ("&U,BTF2XlZyyKng-"), "UTF-7 encoded text does not match the expected value."); } [Test] @@ -124,10 +141,10 @@ public void TestDecodeSurrogatePair () const string example = "&2DzcHA-"; var decoded = ImapEncoding.Decode (example); - Assert.AreEqual ("\ud83c\udc1c", decoded, "UTF-7 decoded text does not match the expected value."); + Assert.That (decoded, Is.EqualTo ("\ud83c\udc1c"), "UTF-7 decoded text does not match the expected value."); var encoded = ImapEncoding.Encode (decoded); - Assert.AreEqual ("&2DzcHA-", encoded, "UTF-7 encoded text does not match the expected value."); + Assert.That (encoded, Is.EqualTo ("&2DzcHA-"), "UTF-7 encoded text does not match the expected value."); } } } diff --git a/UnitTests/Net/Imap/ImapEngineTests.cs b/UnitTests/Net/Imap/ImapEngineTests.cs new file mode 100644 index 0000000000..0ed2ec4482 --- /dev/null +++ b/UnitTests/Net/Imap/ImapEngineTests.cs @@ -0,0 +1,707 @@ +// +// ImapEngineTests.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System.Text; +using System.Globalization; + +using MailKit; +using MailKit.Net.Imap; + +namespace UnitTests.Net.Imap { + [TestFixture] + public class ImapEngineTests + { + [TestCase ('*', (int) ImapTokenType.Asterisk, (int) ImapTokenType.Atom)] + [TestCase ("ATOM", (int) ImapTokenType.Atom, (int) ImapTokenType.Asterisk)] + [TestCase ("\\Flagged", (int) ImapTokenType.Flag, (int) ImapTokenType.QString)] + [TestCase ("QSTRING", (int) ImapTokenType.QString, (int) ImapTokenType.Atom)] + [TestCase (123456, (int) ImapTokenType.Literal, (int) ImapTokenType.Atom)] + [TestCase ("NIL", (int) ImapTokenType.Nil, (int) ImapTokenType.QString)] + + public void TestAssertToken (object value, int actual, int expected) + { + using (var builder = new ByteArrayBuilder (64)) { + ImapToken token; + + if (value is string str) { + foreach (var c in str) + builder.Append ((byte) c); + + token = ImapToken.Create ((ImapTokenType) actual, builder); + } else if (value is char c) { + token = ImapToken.Create ((ImapTokenType) actual, c); + } else if (value is int literal) { + token = ImapToken.Create ((ImapTokenType) actual, literal); + } else { + return; + } + + Assert.Throws (() => ImapEngine.AssertToken (token, (ImapTokenType) expected, "Unexpected token: {0}", token)); + } + } + + [Test] + public void TestParseNumber () + { + using (var builder = new ByteArrayBuilder (64)) { + ImapToken token; + uint value; + + builder.Append ((byte) '0'); + + token = ImapToken.Create (ImapTokenType.Atom, builder); + value = ImapEngine.ParseNumber (token, false, "Unexpected number: {0}", token); + Assert.That (value, Is.EqualTo (0), "number"); + + Assert.Throws (() => ImapEngine.ParseNumber (token, true, "Unexpected number: {0}", token), "nz-number"); + + builder.Clear (); + var max = uint.MaxValue.ToString (CultureInfo.InvariantCulture); + for (int i = 0; i < max.Length; i++) + builder.Append ((byte) max[i]); + + token = ImapToken.Create (ImapTokenType.Atom, builder); + value = ImapEngine.ParseNumber (token, false, "Unexpected number: {0}", token); + Assert.That (value, Is.EqualTo (uint.MaxValue), "max number"); + } + } + + [Test] + public void TestParseNumber64 () + { + using (var builder = new ByteArrayBuilder (64)) { + ImapToken token; + ulong value; + + builder.Append ((byte) '0'); + + token = ImapToken.Create (ImapTokenType.Atom, builder); + value = ImapEngine.ParseNumber64 (token, false, "Unexpected number: {0}", token); + Assert.That (value, Is.EqualTo (0), "number64"); + + Assert.Throws (() => ImapEngine.ParseNumber64 (token, true, "Unexpected number: {0}", token), "nz-number64"); + + builder.Clear (); + var max = ulong.MaxValue.ToString (CultureInfo.InvariantCulture); + for (int i = 0; i < max.Length; i++) + builder.Append ((byte) max[i]); + + token = ImapToken.Create (ImapTokenType.Atom, builder); + value = ImapEngine.ParseNumber64 (token, false, "Unexpected number: {0}", token); + Assert.That (value, Is.EqualTo (ulong.MaxValue), "max number64"); + } + } + + [Test] + public void TestParseUidSet () + { + using (var builder = new ByteArrayBuilder (64)) { + UniqueId? min, max; + UniqueIdSet uids; + ImapToken token; + + builder.Append ((byte) '0'); + + token = ImapToken.Create (ImapTokenType.Atom, builder); + Assert.Throws (() => ImapEngine.ParseUidSet (token, 0, out min, out max, "Unexpected uid-set: {0}", token), "0"); + + builder.Clear (); + var bytes = Encoding.ASCII.GetBytes ("1:500"); + for (int i = 0; i < bytes.Length; i++) + builder.Append (bytes[i]); + + token = ImapToken.Create (ImapTokenType.Atom, builder); + uids = ImapEngine.ParseUidSet (token, 0, out min, out max, "Unexpected uid-set: {0}", token); + Assert.That (uids.ToString (), Is.EqualTo ("1:500"), "uid-set"); + Assert.That (min.ToString (), Is.EqualTo ("1"), "min"); + Assert.That (max.ToString (), Is.EqualTo ("500"), "max"); + } + } + + [Test] + public void TestGetResponseCodeType () + { + foreach (ImapResponseCodeType type in Enum.GetValues (typeof (ImapResponseCodeType))) { + string atom; + + switch (type) { + case ImapResponseCodeType.ReadOnly: atom = "READ-ONLY"; break; + case ImapResponseCodeType.ReadWrite: atom = "READ-WRITE"; break; + case ImapResponseCodeType.UnknownCte: atom = "UNKNOWN-CTE"; break; + case ImapResponseCodeType.UndefinedFilter: atom = "UNDEFINED-FILTER"; break; + default: atom = type.ToString ().ToUpperInvariant (); break; + } + + var result = ImapEngine.GetResponseCodeType (atom); + Assert.That (result, Is.EqualTo (type)); + } + } + + [Test] + public void TestParseResponseCodeBadCharset () + { + const string text = "BADCHARSET (US-ASCII \"iso-8859-1\" UTF-8)] This is some free-form text\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + ImapResponseCode respCode; + + engine.SetStream (tokenizer); + + try { + respCode = engine.ParseResponseCode (true, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing RESP-CODE failed: {ex}"); + return; + } + + Assert.That (respCode.Type, Is.EqualTo (ImapResponseCodeType.BadCharset)); + Assert.That (respCode.Message, Is.EqualTo ("This is some free-form text")); + + Assert.That (engine.SupportedCharsets, Has.Count.EqualTo (3)); + Assert.That (engine.SupportedCharsets, Does.Contain ("US-ASCII"), "US-ASCII"); + Assert.That (engine.SupportedCharsets, Does.Contain ("iso-8859-1"), "iso-8859-1"); + Assert.That (engine.SupportedCharsets, Does.Contain ("UTF-8"), "UTF-8"); + } + } + } + } + + [Test] + public async Task TestParseResponseCodeBadCharsetAsync () + { + const string text = "BADCHARSET (US-ASCII \"iso-8859-1\" UTF-8)] This is some free-form text\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + ImapResponseCode respCode; + + engine.SetStream (tokenizer); + + try { + respCode = await engine.ParseResponseCodeAsync (true, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing RESP-CODE failed: {ex}"); + return; + } + + Assert.That (respCode.Type, Is.EqualTo (ImapResponseCodeType.BadCharset)); + Assert.That (respCode.Message, Is.EqualTo ("This is some free-form text")); + + Assert.That (engine.SupportedCharsets, Has.Count.EqualTo (3)); + Assert.That (engine.SupportedCharsets, Does.Contain ("US-ASCII"), "US-ASCII"); + Assert.That (engine.SupportedCharsets, Does.Contain ("iso-8859-1"), "iso-8859-1"); + Assert.That (engine.SupportedCharsets, Does.Contain ("UTF-8"), "UTF-8"); + } + } + } + } + + [Test] + public void TestParseResponseCodeBadUrl () + { + const string text = "BADURL \"/INBOX;UIDVALIDITY=785799047/;UID=113330;section=1.5.9\"] CATENATE append has failed, one message expunged\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + ImapResponseCode respCode; + + engine.SetStream (tokenizer); + + try { + respCode = engine.ParseResponseCode (true, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing RESP-CODE failed: {ex}"); + return; + } + + Assert.That (respCode.Type, Is.EqualTo (ImapResponseCodeType.BadUrl)); + Assert.That (respCode.Message, Is.EqualTo ("CATENATE append has failed, one message expunged")); + + var badurl = (BadUrlResponseCode) respCode; + Assert.That (badurl.BadUrl, Is.EqualTo ("/INBOX;UIDVALIDITY=785799047/;UID=113330;section=1.5.9")); + } + } + } + } + + [Test] + public async Task TestParseResponseCodeBadUrlAsync () + { + const string text = "BADURL \"/INBOX;UIDVALIDITY=785799047/;UID=113330;section=1.5.9\"] CATENATE append has failed, one message expunged\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + ImapResponseCode respCode; + + engine.SetStream (tokenizer); + + try { + respCode = await engine.ParseResponseCodeAsync (true, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing RESP-CODE failed: {ex}"); + return; + } + + Assert.That (respCode.Type, Is.EqualTo (ImapResponseCodeType.BadUrl)); + Assert.That (respCode.Message, Is.EqualTo ("CATENATE append has failed, one message expunged")); + + var badurl = (BadUrlResponseCode) respCode; + Assert.That (badurl.BadUrl, Is.EqualTo ("/INBOX;UIDVALIDITY=785799047/;UID=113330;section=1.5.9")); + } + } + } + } + + [Test] + public void TestParseResponseCodeMaxConvertMessages () + { + const string text = "MAXCONVERTMESSAGES 1] This is some free-form text\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + ImapResponseCode respCode; + + engine.SetStream (tokenizer); + + try { + respCode = engine.ParseResponseCode (true, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing RESP-CODE failed: {ex}"); + return; + } + + Assert.That (respCode.Type, Is.EqualTo (ImapResponseCodeType.MaxConvertMessages)); + Assert.That (respCode.Message, Is.EqualTo ("This is some free-form text")); + + var maxconvert = (MaxConvertResponseCode) respCode; + Assert.That (maxconvert.MaxConvert, Is.EqualTo (1)); + } + } + } + } + + [Test] + public async Task TestParseResponseCodeMaxConvertMessagesAsync () + { + const string text = "MAXCONVERTMESSAGES 1] This is some free-form text\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + ImapResponseCode respCode; + + engine.SetStream (tokenizer); + + try { + respCode = await engine.ParseResponseCodeAsync (true, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing RESP-CODE failed: {ex}"); + return; + } + + Assert.That (respCode.Type, Is.EqualTo (ImapResponseCodeType.MaxConvertMessages)); + Assert.That (respCode.Message, Is.EqualTo ("This is some free-form text")); + + var maxconvert = (MaxConvertResponseCode) respCode; + Assert.That (maxconvert.MaxConvert, Is.EqualTo (1)); + } + } + } + } + + [Test] + public void TestParseResponseCodeMaxConvertParts () + { + const string text = "MAXCONVERTPARTS 1] This is some free-form text\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + ImapResponseCode respCode; + + engine.SetStream (tokenizer); + + try { + respCode = engine.ParseResponseCode (true, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing RESP-CODE failed: {ex}"); + return; + } + + Assert.That (respCode.Type, Is.EqualTo (ImapResponseCodeType.MaxConvertParts)); + Assert.That (respCode.Message, Is.EqualTo ("This is some free-form text")); + + var maxconvert = (MaxConvertResponseCode) respCode; + Assert.That (maxconvert.MaxConvert, Is.EqualTo (1)); + } + } + } + } + + [Test] + public async Task TestParseResponseCodeMaxConvertPartsAsync () + { + const string text = "MAXCONVERTPARTS 1] This is some free-form text\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + ImapResponseCode respCode; + + engine.SetStream (tokenizer); + + try { + respCode = await engine.ParseResponseCodeAsync (true, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing RESP-CODE failed: {ex}"); + return; + } + + Assert.That (respCode.Type, Is.EqualTo (ImapResponseCodeType.MaxConvertParts)); + Assert.That (respCode.Message, Is.EqualTo ("This is some free-form text")); + + var maxconvert = (MaxConvertResponseCode) respCode; + Assert.That (maxconvert.MaxConvert, Is.EqualTo (1)); + } + } + } + } + + [Test] + public void TestParseResponseCodeNoUpdate () + { + const string text = "NOUPDATE \"B02\"] Too many contexts\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + ImapResponseCode respCode; + + engine.SetStream (tokenizer); + + try { + respCode = engine.ParseResponseCode (false, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing RESP-CODE failed: {ex}"); + return; + } + + Assert.That (respCode.Type, Is.EqualTo (ImapResponseCodeType.NoUpdate)); + Assert.That (respCode.Message, Is.EqualTo ("Too many contexts")); + + var noupdate = (NoUpdateResponseCode) respCode; + Assert.That (noupdate.Tag, Is.EqualTo ("B02")); + } + } + } + } + + [Test] + public async Task TestParseResponseCodeNoUpdateAsync () + { + const string text = "NOUPDATE \"B02\"] Too many contexts\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + ImapResponseCode respCode; + + engine.SetStream (tokenizer); + + try { + respCode = await engine.ParseResponseCodeAsync (false, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing RESP-CODE failed: {ex}"); + return; + } + + Assert.That (respCode.Type, Is.EqualTo (ImapResponseCodeType.NoUpdate)); + Assert.That (respCode.Message, Is.EqualTo ("Too many contexts")); + + var noupdate = (NoUpdateResponseCode) respCode; + Assert.That (noupdate.Tag, Is.EqualTo ("B02")); + } + } + } + } + + [Test] + public void TestParseResponseCodeNewName () + { + const string text = "NEWNAME OldName NewName] This is some free-form text\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + ImapResponseCode respCode; + + engine.SetStream (tokenizer); + + try { + respCode = engine.ParseResponseCode (true, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing RESP-CODE failed: {ex}"); + return; + } + + Assert.That (respCode.Type, Is.EqualTo (ImapResponseCodeType.NewName)); + Assert.That (respCode.Message, Is.EqualTo ("This is some free-form text")); + + var newname = (NewNameResponseCode) respCode; + Assert.That (newname.OldName, Is.EqualTo ("OldName")); + Assert.That (newname.NewName, Is.EqualTo ("NewName")); + } + } + } + } + + [Test] + public async Task TestParseResponseCodeNewNameAsync () + { + const string text = "NEWNAME OldName NewName] This is some free-form text\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + ImapResponseCode respCode; + + engine.SetStream (tokenizer); + + try { + respCode = await engine.ParseResponseCodeAsync (true, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing RESP-CODE failed: {ex}"); + return; + } + + Assert.That (respCode.Type, Is.EqualTo (ImapResponseCodeType.NewName)); + Assert.That (respCode.Message, Is.EqualTo ("This is some free-form text")); + + var newname = (NewNameResponseCode) respCode; + Assert.That (newname.OldName, Is.EqualTo ("OldName")); + Assert.That (newname.NewName, Is.EqualTo ("NewName")); + } + } + } + } + + [Test] + public void TestParseResponseCodeUndefinedFilter () + { + const string text = "UNDEFINED-FILTER filter-name] This is some free-form text\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + ImapResponseCode respCode; + + engine.SetStream (tokenizer); + + try { + respCode = engine.ParseResponseCode (true, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing RESP-CODE failed: {ex}"); + return; + } + + Assert.That (respCode.Type, Is.EqualTo (ImapResponseCodeType.UndefinedFilter)); + Assert.That (respCode.Message, Is.EqualTo ("This is some free-form text")); + + var undefined = (UndefinedFilterResponseCode) respCode; + Assert.That (undefined.Name, Is.EqualTo ("filter-name")); + } + } + } + } + + [Test] + public async Task TestParseResponseCodeUndefinedFilterAsync () + { + const string text = "UNDEFINED-FILTER filter-name] This is some free-form text\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + ImapResponseCode respCode; + + engine.SetStream (tokenizer); + + try { + respCode = await engine.ParseResponseCodeAsync (true, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing RESP-CODE failed: {ex}"); + return; + } + + Assert.That (respCode.Type, Is.EqualTo (ImapResponseCodeType.UndefinedFilter)); + Assert.That (respCode.Message, Is.EqualTo ("This is some free-form text")); + + var undefined = (UndefinedFilterResponseCode) respCode; + Assert.That (undefined.Name, Is.EqualTo ("filter-name")); + } + } + } + } + + void TestGreetingDetection (string server, string fileName, ImapQuirksMode expected) + { + using (var input = GetType ().Assembly.GetManifestResourceStream ("UnitTests.Net.Imap.Resources." + server + "." + fileName)) { + using (var tokenizer = new ImapStream (input, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + try { + engine.Connect (tokenizer, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing greeting failed: {ex}"); + return; + } + + Assert.That (engine.QuirksMode, Is.EqualTo (expected)); + } + } + } + } + + async Task TestGreetingDetectionAsync (string server, string fileName, ImapQuirksMode expected) + { + using (var input = GetType ().Assembly.GetManifestResourceStream ("UnitTests.Net.Imap.Resources." + server + "." + fileName)) { + using (var tokenizer = new ImapStream (input, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + try { + await engine.ConnectAsync (tokenizer, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing greeting failed: {ex}"); + return; + } + + Assert.That (engine.QuirksMode, Is.EqualTo (expected)); + } + } + } + } + + [Test] + public void TestCourierImapDetection () + { + TestGreetingDetection ("courier", "greeting.txt", ImapQuirksMode.Courier); + } + + [Test] + public Task TestCourierImapDetectionAsync () + { + return TestGreetingDetectionAsync ("courier", "greeting.txt", ImapQuirksMode.Courier); + } + + [Test] + public void TestCyrusImapDetection () + { + TestGreetingDetection ("cyrus", "greeting.txt", ImapQuirksMode.Cyrus); + } + + [Test] + public Task TestCyrusImapDetectionAsync () + { + return TestGreetingDetectionAsync ("cyrus", "greeting.txt", ImapQuirksMode.Cyrus); + } + + [Test] + public void TestDominoImapDetection () + { + TestGreetingDetection ("domino", "greeting.txt", ImapQuirksMode.Domino); + } + + [Test] + public Task TestDominoImapDetectionAsync () + { + return TestGreetingDetectionAsync ("domino", "greeting.txt", ImapQuirksMode.Domino); + } + + [Test] + public void TestDovecotImapDetection () + { + TestGreetingDetection ("dovecot", "greeting.txt", ImapQuirksMode.Dovecot); + } + + [Test] + public Task TestDovecotImapDetectionAsync () + { + return TestGreetingDetectionAsync ("dovecot", "greeting.txt", ImapQuirksMode.Dovecot); + } + + [Test] + public void TestExchangeImapDetection () + { + TestGreetingDetection ("exchange", "greeting.txt", ImapQuirksMode.Exchange); + } + + [Test] + public Task TestExchangeImapDetectionAsync () + { + return TestGreetingDetectionAsync ("exchange", "greeting.txt", ImapQuirksMode.Exchange); + } + + [Test] + public void TestExchange2003ImapDetection () + { + TestGreetingDetection ("exchange", "greeting-2003.txt", ImapQuirksMode.Exchange2003); + } + + [Test] + public Task TestExchange2003ImapDetectionAsync () + { + return TestGreetingDetectionAsync ("exchange", "greeting-2003.txt", ImapQuirksMode.Exchange2003); + } + + [Test] + public void TestExchange2007ImapDetection () + { + TestGreetingDetection ("exchange", "greeting-2007.txt", ImapQuirksMode.Exchange2007); + } + + [Test] + public Task TestExchange2007ImapDetectionAsync () + { + return TestGreetingDetectionAsync ("exchange", "greeting-2007.txt", ImapQuirksMode.Exchange2007); + } + + [Test] + public void TestUWImapDetection () + { + TestGreetingDetection ("uw", "greeting.txt", ImapQuirksMode.UW); + } + + [Test] + public Task TestUWImapDetectionAsync () + { + return TestGreetingDetectionAsync ("uw", "greeting.txt", ImapQuirksMode.UW); + } + } +} diff --git a/UnitTests/Net/Imap/ImapEventGroupTests.cs b/UnitTests/Net/Imap/ImapEventGroupTests.cs new file mode 100644 index 0000000000..02956a3f8e --- /dev/null +++ b/UnitTests/Net/Imap/ImapEventGroupTests.cs @@ -0,0 +1,218 @@ +// +// ImapEventGroupTests.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System.Text; + +using MailKit; +using MailKit.Net.Imap; + +using MimeKit; + +namespace UnitTests.Net.Imap { + [TestFixture] + public class ImapEventGroupTests + { + [Test] + public void TestArgumentExceptions () + { + Assert.Throws (() => new ImapEventGroup (null, new List ())); + Assert.Throws (() => new ImapEventGroup (ImapMailboxFilter.Selected, (IList) null)); + + Assert.Throws (() => new ImapEventGroup (null)); + Assert.Throws (() => new ImapEventGroup (ImapMailboxFilter.Selected, null)); + + Assert.Throws (() => new ImapMailboxFilter.Mailboxes (null)); + Assert.Throws (() => new ImapMailboxFilter.Mailboxes ((IList) null)); + + Assert.Throws (() => new ImapMailboxFilter.Mailboxes ()); + Assert.Throws (() => new ImapMailboxFilter.Mailboxes ((IList) Array.Empty ())); + + Assert.Throws (() => new ImapMailboxFilter.Subtree (null)); + Assert.Throws (() => new ImapMailboxFilter.Subtree ((IList) null)); + + Assert.Throws (() => new ImapMailboxFilter.Subtree ()); + Assert.Throws (() => new ImapMailboxFilter.Subtree ((IList) Array.Empty ())); + + Assert.Throws (() => new ImapEvent.MessageNew (null)); + } + + static void AssertFormatEventGroup (ImapEventGroup eventGroup, string expected, bool expectedNotify) + { + using var engine = new ImapEngine (null); + bool notifySelectedNewExpunge = false; + var command = new StringBuilder (); + var args = new List (); + + if (expected == null) { + Assert.Throws (() => eventGroup.Format (engine, command, args, ref notifySelectedNewExpunge)); + } else { + eventGroup.Format (engine, command, args, ref notifySelectedNewExpunge); + + Assert.That (command.ToString (), Is.EqualTo (expected)); + Assert.That (notifySelectedNewExpunge, Is.EqualTo (expectedNotify), "notifySelectedNewExpunge"); + } + } + + [Test] + public void TestFormatEventGroup_None () + { + var eventGroup = new ImapEventGroup (ImapMailboxFilter.Inboxes, Array.Empty ()); + + AssertFormatEventGroup (eventGroup, "(INBOXES NONE)", false); + } + + [Test] + public void TestFormatEventGroup_AnnotationChange_Requires_MessageNew_And_MessageExpunge () + { + var eventGroup = new ImapEventGroup (ImapMailboxFilter.Inboxes, ImapEvent.AnnotationChange); + + AssertFormatEventGroup (eventGroup, null, false); + } + + [Test] + public void TestFormatEventGroup_AnnotationChange_MessageNew_Requires_MessageExpunge () + { + var eventGroup = new ImapEventGroup (ImapMailboxFilter.Inboxes, ImapEvent.AnnotationChange, new ImapEvent.MessageNew (MessageSummaryItems.None)); + + AssertFormatEventGroup (eventGroup, null, false); + } + + [Test] + public void TestFormatEventGroup_AnnotationChange_MessageExpunge_Requires_MessageNew () + { + var eventGroup = new ImapEventGroup (ImapMailboxFilter.Inboxes, ImapEvent.AnnotationChange, ImapEvent.MessageExpunge); + + AssertFormatEventGroup (eventGroup, null, false); + } + + [Test] + public void TestFormatEventGroup_FlagChange_Requires_MessageNew_And_MessageExpunge () + { + var eventGroup = new ImapEventGroup (ImapMailboxFilter.Inboxes, ImapEvent.FlagChange); + + AssertFormatEventGroup (eventGroup, null, false); + } + + [Test] + public void TestFormatEventGroup_FlagChange_MessageNew_Requires_MessageExpunge () + { + var eventGroup = new ImapEventGroup (ImapMailboxFilter.Inboxes, ImapEvent.FlagChange, new ImapEvent.MessageNew (MessageSummaryItems.None)); + + AssertFormatEventGroup (eventGroup, null, false); + } + + [Test] + public void TestFormatEventGroup_FlagChange_MessageExpunge_Requires_MessageNew () + { + var eventGroup = new ImapEventGroup (ImapMailboxFilter.Inboxes, ImapEvent.FlagChange, ImapEvent.MessageExpunge); + + AssertFormatEventGroup (eventGroup, null, false); + } + + [Test] + public void TestFormatEventGroup_FlagChange_MessageNew_MessageExpunge_AnnotationChange () + { + var eventGroup = new ImapEventGroup (ImapMailboxFilter.Inboxes, ImapEvent.FlagChange, new ImapEvent.MessageNew (MessageSummaryItems.None), ImapEvent.MessageExpunge, ImapEvent.AnnotationChange); + + AssertFormatEventGroup (eventGroup, "(INBOXES (FlagChange MessageNew MessageExpunge AnnotationChange))", false); + } + + [Test] + public void TestFormatEventGroup_MessageExpunge_Requires_MessageNew () + { + var eventGroup = new ImapEventGroup (ImapMailboxFilter.Inboxes, ImapEvent.MessageExpunge); + + AssertFormatEventGroup (eventGroup, null, false); + } + + [Test] + public void TestFormatEventGroup_MessageNew_Requires_MessageExpunge () + { + var eventGroup = new ImapEventGroup (ImapMailboxFilter.Inboxes, new ImapEvent.MessageNew (MessageSummaryItems.None)); + + AssertFormatEventGroup (eventGroup, null, false); + } + + [Test] + public void TestFormatEventGroup_MessageNew_MessageExpunge () + { + var eventGroup = new ImapEventGroup (ImapMailboxFilter.Inboxes, new ImapEvent.MessageNew (MessageSummaryItems.None), ImapEvent.MessageExpunge); + + AssertFormatEventGroup (eventGroup, "(INBOXES (MessageNew MessageExpunge))", false); + } + + [Test] + public void TestFormatEventGroup_MessageNew_Headers_Requires_Selected () + { + var headers = new HashSet (new HeaderId[] { HeaderId.From, HeaderId.Subject, HeaderId.Date }); + var eventGroup = new ImapEventGroup (ImapMailboxFilter.Inboxes, new ImapEvent.MessageNew (MessageSummaryItems.None, headers), ImapEvent.MessageExpunge); + + AssertFormatEventGroup (eventGroup, null, false); + } + + [Test] + public void TestFormatEventGroup_MessageNew_Items_Requires_Selected () + { + var eventGroup = new ImapEventGroup (ImapMailboxFilter.Inboxes, new ImapEvent.MessageNew (MessageSummaryItems.Full), ImapEvent.MessageExpunge); + + AssertFormatEventGroup (eventGroup, null, false); + } + + [Test] + public void TestFormatEventGroup_MessageNew_WithSpecificHeaderIds () + { + var headers = new HashSet (new HeaderId[] { HeaderId.From, HeaderId.Subject, HeaderId.Date }); + var eventGroup = new ImapEventGroup (ImapMailboxFilter.Selected, new ImapEvent.MessageNew (MessageSummaryItems.None, headers), ImapEvent.MessageExpunge); + + AssertFormatEventGroup (eventGroup, "(SELECTED (MessageNew (BODY.PEEK[HEADER.FIELDS (FROM SUBJECT DATE)]) MessageExpunge))", true); + } + + [Test] + public void TestFormatEventGroup_MessageNew_WithSpecificHeaderNames () + { + var headers = new HashSet (new string[] { "From", "Subject", "Date" }); + var eventGroup = new ImapEventGroup (ImapMailboxFilter.Selected, new ImapEvent.MessageNew (MessageSummaryItems.None, headers), ImapEvent.MessageExpunge); + + AssertFormatEventGroup (eventGroup, "(SELECTED (MessageNew (BODY.PEEK[HEADER.FIELDS (FROM SUBJECT DATE)]) MessageExpunge))", true); + } + + [Test] + public void TestFormatEventGroup_Selected_Requires_OnlyMessageEvents () + { + var eventGroup = new ImapEventGroup (ImapMailboxFilter.Selected, ImapEvent.ServerMetadataChange); + + AssertFormatEventGroup (eventGroup, null, false); + } + + [Test] + public void TestFormatEventGroup_SelectedDelayed_Requires_OnlyMessageEvents () + { + var eventGroup = new ImapEventGroup (ImapMailboxFilter.SelectedDelayed, ImapEvent.ServerMetadataChange); + + AssertFormatEventGroup (eventGroup, null, false); + } + } +} diff --git a/UnitTests/Net/Imap/ImapFolderAnnotationsTests.cs b/UnitTests/Net/Imap/ImapFolderAnnotationsTests.cs new file mode 100644 index 0000000000..b04803210c --- /dev/null +++ b/UnitTests/Net/Imap/ImapFolderAnnotationsTests.cs @@ -0,0 +1,1242 @@ +// +// ImapFolderAnnotationsTests.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System.Net; +using System.Text; +using System.Globalization; + +using MimeKit; + +using MailKit; +using MailKit.Search; +using MailKit.Security; +using MailKit.Net.Imap; + +namespace UnitTests.Net.Imap { + [TestFixture] + public class ImapFolderAnnotationsTests + { + static readonly Encoding Latin1 = Encoding.GetEncoding (28591); + + static Stream GetResourceStream (string name) + { + return typeof (ImapFolderAnnotationsTests).Assembly.GetManifestResourceStream ("UnitTests.Net.Imap.Resources." + name); + } + + [Test] + public void TestArgumentExceptions () + { + var commands = new List { + new ImapReplayCommand ("", "dovecot.greeting.txt"), + new ImapReplayCommand ("A00000000 LOGIN username password\r\n", "dovecot.authenticate+annotate.txt"), + new ImapReplayCommand ("A00000001 NAMESPACE\r\n", "dovecot.namespace.txt"), + new ImapReplayCommand ("A00000002 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-inbox.txt"), + new ImapReplayCommand ("A00000003 LIST (SPECIAL-USE) \"\" \"*\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-special-use.txt"), + new ImapReplayCommand ("A00000004 SELECT INBOX (CONDSTORE ANNOTATE)\r\n", "common.select-inbox-annotate.txt") + }; + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + var credentials = new NetworkCredential ("username", "password"); + + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + client.Authenticate (credentials); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Inbox.SyncRoot, Is.InstanceOf (), "SyncRoot"); + + var inbox = (ImapFolder) client.Inbox; + inbox.Open (FolderAccess.ReadWrite); + + Assert.That (inbox.AnnotationAccess, Is.EqualTo (AnnotationAccess.ReadWrite), "AnnotationAccess"); + Assert.That (inbox.AnnotationScopes, Is.EqualTo (AnnotationScope.Shared), "AnnotationScopes"); + Assert.That (inbox.MaxAnnotationSize, Is.EqualTo (20480), "MaxAnnotationSize"); + + var annotations = new List (new[] { + new Annotation (AnnotationEntry.AltSubject) + }); + annotations[0].Properties.Add (AnnotationAttribute.SharedValue, "value"); + + // Store + Assert.Throws (() => inbox.Store (-1, annotations)); + Assert.ThrowsAsync (() => inbox.StoreAsync (-1, annotations)); + Assert.Throws (() => inbox.Store (0, (IList) null)); + Assert.ThrowsAsync (() => inbox.StoreAsync (0, (IList) null)); + + Assert.Throws (() => inbox.Store (UniqueId.Invalid, annotations)); + Assert.ThrowsAsync (() => inbox.StoreAsync (UniqueId.Invalid, annotations)); + Assert.Throws (() => inbox.Store (UniqueId.MinValue, (IList) null)); + Assert.ThrowsAsync (() => inbox.StoreAsync (UniqueId.MinValue, (IList) null)); + + Assert.Throws (() => inbox.Store ((IList) null, annotations)); + Assert.ThrowsAsync (() => inbox.StoreAsync ((IList) null, annotations)); + Assert.Throws (() => inbox.Store (new int[] { 0 }, (IList) null)); + Assert.ThrowsAsync (() => inbox.StoreAsync (new int[] { 0 }, (IList) null)); + Assert.Throws (() => inbox.Store ((IList) null, 1, annotations)); + Assert.ThrowsAsync (() => inbox.StoreAsync ((IList) null, 1, annotations)); + Assert.Throws (() => inbox.Store (new int[] { 0 }, 1, (IList) null)); + Assert.ThrowsAsync (() => inbox.StoreAsync (new int[] { 0 }, 1, (IList) null)); + + Assert.Throws (() => inbox.Store ((IList) null, annotations)); + Assert.ThrowsAsync (() => inbox.StoreAsync ((IList) null, annotations)); + Assert.Throws (() => inbox.Store (UniqueIdRange.All, (IList) null)); + Assert.ThrowsAsync (() => inbox.StoreAsync (UniqueIdRange.All, (IList) null)); + Assert.Throws (() => inbox.Store ((IList) null, 1, annotations)); + Assert.ThrowsAsync (() => inbox.StoreAsync ((IList) null, 1, annotations)); + Assert.Throws (() => inbox.Store (UniqueIdRange.All, 1, (IList) null)); + Assert.ThrowsAsync (() => inbox.StoreAsync (UniqueIdRange.All, 1, (IList) null)); + + client.Disconnect (false); + } + } + + [Test] + public void TestNotSupportedExceptions () + { + var commands = new List { + new ImapReplayCommand ("", "dovecot.greeting.txt"), + new ImapReplayCommand ("A00000000 LOGIN username password\r\n", "dovecot.authenticate+annotate.txt"), + new ImapReplayCommand ("A00000001 NAMESPACE\r\n", "dovecot.namespace.txt"), + new ImapReplayCommand ("A00000002 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-inbox.txt"), + new ImapReplayCommand ("A00000003 LIST (SPECIAL-USE) \"\" \"*\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-special-use.txt"), + new ImapReplayCommand ("A00000004 SELECT INBOX (CONDSTORE ANNOTATE)\r\n", "common.select-inbox.txt"), + new ImapReplayCommand ("A00000005 SELECT INBOX (ANNOTATE)\r\n", "common.select-inbox-annotate-no-modseq.txt") + }; + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + var credentials = new NetworkCredential ("username", "password"); + + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + client.Authenticate (credentials); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Inbox.SyncRoot, Is.InstanceOf (), "SyncRoot"); + + var inbox = (ImapFolder) client.Inbox; + inbox.Open (FolderAccess.ReadWrite); + + Assert.That (inbox.AnnotationAccess, Is.EqualTo (AnnotationAccess.None), "AnnotationAccess"); + Assert.That (inbox.AnnotationScopes, Is.EqualTo (AnnotationScope.None), "AnnotationScopes"); + Assert.That (inbox.MaxAnnotationSize, Is.EqualTo (0), "MaxAnnotationSize"); + + var annotations = new List (new[] { + new Annotation (AnnotationEntry.AltSubject) + }); + annotations[0].Properties.Add (AnnotationAttribute.SharedValue, "value"); + + // verify NotSupportedException for storing annotations + Assert.Throws (() => inbox.Store (0, annotations)); + Assert.ThrowsAsync (() => inbox.StoreAsync (0, annotations)); + + Assert.Throws (() => inbox.Store (UniqueId.MinValue, annotations)); + Assert.ThrowsAsync (() => inbox.StoreAsync (UniqueId.MinValue, annotations)); + + Assert.Throws (() => inbox.Store (new int[] { 0 }, 1, annotations)); + Assert.ThrowsAsync (() => inbox.StoreAsync (new int[] { 0 }, 1, annotations)); + + Assert.Throws (() => inbox.Store (UniqueIdRange.All, 1, annotations)); + Assert.ThrowsAsync (() => inbox.StoreAsync (UniqueIdRange.All, 1, annotations)); + + // disable CONDSTORE and verify that we get NotSupportedException when we send modseq + client.Capabilities &= ~ImapCapabilities.CondStore; + inbox.Open (FolderAccess.ReadWrite); + + Assert.That (inbox.AnnotationAccess, Is.EqualTo (AnnotationAccess.ReadWrite), "AnnotationAccess"); + Assert.That (inbox.AnnotationScopes, Is.EqualTo (AnnotationScope.Shared), "AnnotationScopes"); + Assert.That (inbox.MaxAnnotationSize, Is.EqualTo (20480), "MaxAnnotationSize"); + + Assert.Throws (() => inbox.Store (new int[] { 0 }, 1, annotations)); + Assert.ThrowsAsync (() => inbox.StoreAsync (new int[] { 0 }, 1, annotations)); + + Assert.Throws (() => inbox.Store (UniqueIdRange.All, 1, annotations)); + Assert.ThrowsAsync (() => inbox.StoreAsync (UniqueIdRange.All, 1, annotations)); + + client.Disconnect (false); + } + } + + [Test] + public void TestChangingAnnotationsOnEmptyListOfMessages () + { + var commands = new List { + new ImapReplayCommand ("", "dovecot.greeting.txt"), + new ImapReplayCommand ("A00000000 LOGIN username password\r\n", "dovecot.authenticate+annotate.txt"), + new ImapReplayCommand ("A00000001 NAMESPACE\r\n", "dovecot.namespace.txt"), + new ImapReplayCommand ("A00000002 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-inbox.txt"), + new ImapReplayCommand ("A00000003 LIST (SPECIAL-USE) \"\" \"*\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-special-use.txt"), + new ImapReplayCommand ("A00000004 SELECT INBOX (CONDSTORE ANNOTATE)\r\n", "common.select-inbox-annotate.txt") + }; + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + var credentials = new NetworkCredential ("username", "password"); + + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + client.Authenticate (credentials); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Inbox.SyncRoot, Is.InstanceOf (), "SyncRoot"); + + var inbox = (ImapFolder) client.Inbox; + inbox.Open (FolderAccess.ReadWrite); + + Assert.That (inbox.AnnotationAccess, Is.EqualTo (AnnotationAccess.ReadWrite), "AnnotationAccess"); + Assert.That (inbox.AnnotationScopes, Is.EqualTo (AnnotationScope.Shared), "AnnotationScopes"); + Assert.That (inbox.MaxAnnotationSize, Is.EqualTo (20480), "MaxAnnotationSize"); + + var annotations = new List (new[] { + new Annotation (AnnotationEntry.AltSubject) + }); + annotations[0].Properties.Add (AnnotationAttribute.SharedValue, "value"); + + ulong modseq = 409601020304; + var uids = Array.Empty (); + var indexes = Array.Empty (); + IList unmodifiedUids; + IList unmodifiedIndexes; + + unmodifiedIndexes = inbox.Store (indexes, modseq, annotations); + Assert.That (unmodifiedIndexes, Is.Empty); + + unmodifiedUids = inbox.Store (uids, modseq, annotations); + Assert.That (unmodifiedUids, Is.Empty); + + client.Disconnect (false); + } + } + + static IList CreateAppendWithAnnotationsCommands (bool withInternalDates, out List messages, out List flags, out List internalDates, out List annotations) + { + var commands = new List { + new ImapReplayCommand ("", "dovecot.greeting.txt"), + new ImapReplayCommand ("A00000000 LOGIN username password\r\n", "dovecot.authenticate+annotate.txt"), + new ImapReplayCommand ("A00000001 NAMESPACE\r\n", "dovecot.namespace.txt"), + new ImapReplayCommand ("A00000002 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-inbox.txt"), + new ImapReplayCommand ("A00000003 LIST (SPECIAL-USE) \"\" \"*\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-special-use.txt") + }; + + internalDates = withInternalDates ? new List () : null; + annotations = new List (); + messages = new List (); + flags = new List (); + var command = new StringBuilder (); + int id = 4; + + for (int i = 0; i < 8; i++) { + MimeMessage message; + string latin1; + long length; + + using (var resource = GetResourceStream (string.Format ("common.message.{0}.msg", i))) + message = MimeMessage.Load (resource); + + messages.Add (message); + flags.Add (MessageFlags.Seen); + if (withInternalDates) + internalDates.Add (message.Date); + var annotation = new Annotation (AnnotationEntry.AltSubject); + annotation.Properties[AnnotationAttribute.PrivateValue] = string.Format ("Alternate subject {0}", i); + annotations.Add (annotation); + + using (var stream = new MemoryStream ()) { + var options = FormatOptions.Default.Clone (); + options.NewLineFormat = NewLineFormat.Dos; + options.EnsureNewLine = true; + + message.WriteTo (options, stream); + length = stream.Length; + stream.Position = 0; + + using (var reader = new StreamReader (stream, Latin1)) + latin1 = reader.ReadToEnd (); + } + + var tag = string.Format ("A{0:D8}", id++); + command.Clear (); + + command.AppendFormat ("{0} APPEND INBOX (\\Seen) ", tag); + + if (withInternalDates) + command.AppendFormat ("\"{0}\" ", ImapUtils.FormatInternalDate (message.Date)); + + command.AppendFormat ("ANNOTATION (/altsubject (value.priv \"Alternate subject {0}\")) ", i); + + command.Append ('{').Append (length.ToString (CultureInfo.InvariantCulture)).Append ("+}\r\n").Append (latin1).Append ("\r\n"); + commands.Add (new ImapReplayCommand (command.ToString (), string.Format ("dovecot.append.{0}.txt", i + 1))); + } + + commands.Add (new ImapReplayCommand (string.Format ("A{0:D8} LOGOUT\r\n", id), "gmail.logout.txt")); + + return commands; + } + + [TestCase (false, TestName = "TestAppendWithAnnotations")] + [TestCase (true, TestName = "TestAppendWithAnnotationsAndInternalDates")] + public void TestAppendWithAnnotations (bool withInternalDates) + { + var expectedFlags = MessageFlags.Answered | MessageFlags.Flagged | MessageFlags.Deleted | MessageFlags.Seen | MessageFlags.Draft; + var expectedPermanentFlags = expectedFlags | MessageFlags.UserDefined; + + var commands = CreateAppendWithAnnotationsCommands (withInternalDates, out var messages, out var flags, out var internalDates, out var annotations); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + client.AuthenticationMechanisms.Clear (); + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + for (int i = 0; i < messages.Count; i++) { + UniqueId? uid; + + if (withInternalDates) + uid = client.Inbox.Append (messages[i], flags[i], internalDates[i], new [] { annotations[i] }); + else + uid = client.Inbox.Append (messages[i], flags[i], null, new [] { annotations[i] }); + + Assert.That (uid.HasValue, Is.True, "Expected a UIDAPPEND resp-code"); + Assert.That (uid.Value.Id, Is.EqualTo (i + 1), "Unexpected UID"); + + messages[i].Dispose (); + } + + client.Disconnect (true); + } + } + + [TestCase (false, TestName = "TestAppendWithAnnotationsAsync")] + [TestCase (true, TestName = "TestAppendWithAnnotationsAndInternalDatesAsync")] + public async Task TestAppendWithAnnotationsAsync (bool withInternalDates) + { + var expectedFlags = MessageFlags.Answered | MessageFlags.Flagged | MessageFlags.Deleted | MessageFlags.Seen | MessageFlags.Draft; + var expectedPermanentFlags = expectedFlags | MessageFlags.UserDefined; + + var commands = CreateAppendWithAnnotationsCommands (withInternalDates, out var messages, out var flags, out var internalDates, out var annotations); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + client.AuthenticationMechanisms.Clear (); + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + for (int i = 0; i < messages.Count; i++) { + UniqueId? uid; + + if (withInternalDates) + uid = await client.Inbox.AppendAsync (messages[i], flags[i], internalDates[i], new[] { annotations[i] }); + else + uid = await client.Inbox.AppendAsync (messages[i], flags[i], null, new[] { annotations[i] }); + + Assert.That (uid.HasValue, Is.True, "Expected a UIDAPPEND resp-code"); + Assert.That (uid.Value.Id, Is.EqualTo (i + 1), "Unexpected UID"); + + messages[i].Dispose (); + } + + await client.DisconnectAsync (true); + } + } + + static IList CreateMultiAppendWithAnnotationsCommands (bool withInternalDates, out List requests) + { + var commands = new List { + new ImapReplayCommand ("", "dovecot.greeting.txt"), + new ImapReplayCommand ("A00000000 LOGIN username password\r\n", "dovecot.authenticate+annotate.txt"), + new ImapReplayCommand ("A00000001 NAMESPACE\r\n", "dovecot.namespace.txt"), + new ImapReplayCommand ("A00000002 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-inbox.txt"), + new ImapReplayCommand ("A00000003 LIST (SPECIAL-USE) \"\" \"*\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-special-use.txt") + }; + + var command = new StringBuilder ("A00000004 APPEND INBOX"); + var options = FormatOptions.Default.Clone (); + options.NewLineFormat = NewLineFormat.Dos; + options.EnsureNewLine = true; + int id = 5; + + requests = new List (); + + for (int i = 0; i < 8; i++) { + MimeMessage message; + string latin1; + long length; + + using (var resource = GetResourceStream (string.Format ("common.message.{0}.msg", i))) + message = MimeMessage.Load (resource); + + var request = new AppendRequest (message, MessageFlags.Seen); + requests.Add (request); + + if (withInternalDates) + request.InternalDate = message.Date; + + var annotation = new Annotation (AnnotationEntry.AltSubject); + annotation.Properties[AnnotationAttribute.PrivateValue] = string.Format ("Alternate subject {0}", i); + request.Annotations = new Annotation[] { annotation }; + + using (var stream = new MemoryStream ()) { + message.WriteTo (options, stream); + length = stream.Length; + stream.Position = 0; + + using (var reader = new StreamReader (stream, Latin1)) + latin1 = reader.ReadToEnd (); + } + + command.Append (" (\\Seen) "); + if (withInternalDates) + command.AppendFormat ("\"{0}\" ", ImapUtils.FormatInternalDate (message.Date)); + command.AppendFormat ("ANNOTATION (/altsubject (value.priv \"Alternate subject {0}\")) ", i); + command.Append ('{'); + command.AppendFormat ("{0}+", length); + command.Append ("}\r\n"); + command.Append (latin1); + } + + command.Append ("\r\n"); + commands.Add (new ImapReplayCommand (command.ToString (), "dovecot.multiappend.txt")); + + for (int i = 0; i < requests.Count; i++) { + string latin1; + long length; + + command.Clear (); + command.AppendFormat ("A{0:D8} APPEND INBOX", id++); + + using (var stream = new MemoryStream ()) { + requests[i].Message.WriteTo (options, stream); + length = stream.Length; + stream.Position = 0; + + using (var reader = new StreamReader (stream, Latin1)) + latin1 = reader.ReadToEnd (); + } + + command.Append (" (\\Seen) "); + if (withInternalDates) + command.AppendFormat ("\"{0}\" ", ImapUtils.FormatInternalDate (requests[i].InternalDate.Value)); + command.AppendFormat ("ANNOTATION (/altsubject (value.priv \"Alternate subject {0}\")) ", i); + command.Append ('{'); + command.AppendFormat ("{0}+", length); + command.Append ("}\r\n"); + command.Append (latin1); + command.Append ("\r\n"); + commands.Add (new ImapReplayCommand (command.ToString (), string.Format ("dovecot.append.{0}.txt", i + 1))); + } + + commands.Add (new ImapReplayCommand (string.Format ("A{0:D8} LOGOUT\r\n", id), "gmail.logout.txt")); + + return commands; + } + + [TestCase (false, TestName = "TestMultiAppendWithAnnotations")] + [TestCase (true, TestName = "TestMultiAppendWithAnnotationsAndInternalDates")] + public void TestMultiAppendWithAnnotations (bool withInternalDates) + { + var expectedFlags = MessageFlags.Answered | MessageFlags.Flagged | MessageFlags.Deleted | MessageFlags.Seen | MessageFlags.Draft; + var expectedPermanentFlags = expectedFlags | MessageFlags.UserDefined; + IList uids; + + var commands = CreateMultiAppendWithAnnotationsCommands (withInternalDates, out var requests); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + client.AuthenticationMechanisms.Clear (); + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + // Use MULTIAPPEND to append some test messages + uids = client.Inbox.Append (requests); + Assert.That (uids, Has.Count.EqualTo (8), "Unexpected number of messages appended"); + + for (int i = 0; i < uids.Count; i++) + Assert.That (uids[i].Id, Is.EqualTo (i + 1), "Unexpected UID"); + + // Disable the MULTIAPPEND extension and do it again + client.Capabilities &= ~ImapCapabilities.MultiAppend; + uids = client.Inbox.Append (requests); + + Assert.That (uids, Has.Count.EqualTo (8), "Unexpected number of messages appended"); + + for (int i = 0; i < uids.Count; i++) + Assert.That (uids[i].Id, Is.EqualTo (i + 1), "Unexpected UID"); + + client.Disconnect (true); + + foreach (var request in requests) + request.Message.Dispose (); + } + } + + [TestCase (false, TestName = "TestMultiAppendWithAnnotationsAsync")] + [TestCase (true, TestName = "TestMultiAppendWithAnnotationsAndInternalDatesAsync")] + public async Task TestMultiAppendWithAnnotationsAsync (bool withInternalDates) + { + var expectedFlags = MessageFlags.Answered | MessageFlags.Flagged | MessageFlags.Deleted | MessageFlags.Seen | MessageFlags.Draft; + var expectedPermanentFlags = expectedFlags | MessageFlags.UserDefined; + IList uids; + + var commands = CreateMultiAppendWithAnnotationsCommands (withInternalDates, out var requests); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + client.AuthenticationMechanisms.Clear (); + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + // Use MULTIAPPEND to append some test messages + uids = await client.Inbox.AppendAsync (requests); + Assert.That (uids, Has.Count.EqualTo (8), "Unexpected number of messages appended"); + + for (int i = 0; i < uids.Count; i++) + Assert.That (uids[i].Id, Is.EqualTo (i + 1), "Unexpected UID"); + + // Disable the MULTIAPPEND extension and do it again + client.Capabilities &= ~ImapCapabilities.MultiAppend; + uids = await client.Inbox.AppendAsync (requests); + + Assert.That (uids, Has.Count.EqualTo (8), "Unexpected number of messages appended"); + + for (int i = 0; i < uids.Count; i++) + Assert.That (uids[i].Id, Is.EqualTo (i + 1), "Unexpected UID"); + + await client.DisconnectAsync (true); + + foreach (var request in requests) + request.Message.Dispose (); + } + } + + static IList CreateReplaceWithAnnotationsCommands (bool byUid, out List requests) + { + var commands = new List { + new ImapReplayCommand ("", "dovecot.greeting.txt"), + new ImapReplayCommand ("A00000000 LOGIN username password\r\n", "dovecot.authenticate+annotate+replace.txt"), + new ImapReplayCommand ("A00000001 NAMESPACE\r\n", "dovecot.namespace.txt"), + new ImapReplayCommand ("A00000002 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-inbox.txt"), + new ImapReplayCommand ("A00000003 LIST (SPECIAL-USE) \"\" \"*\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-special-use.txt"), + new ImapReplayCommand ("A00000004 SELECT INBOX (CONDSTORE ANNOTATE)\r\n", "common.select-inbox-annotate.txt") + }; + var command = new StringBuilder (); + int id = 5; + + requests = new List (); + + for (int i = 0; i < 8; i++) { + MimeMessage message; + string latin1; + long length; + + using (var resource = GetResourceStream (string.Format ("common.message.{0}.msg", i))) + message = MimeMessage.Load (resource); + + var annotation = new Annotation (AnnotationEntry.AltSubject); + annotation.Properties[AnnotationAttribute.PrivateValue] = string.Format ("Alternate subject {0}", i); + requests.Add (new ReplaceRequest (message, MessageFlags.Seen) { + Annotations = new Annotation[] { annotation } + }); + + using (var stream = new MemoryStream ()) { + var options = FormatOptions.Default.Clone (); + options.NewLineFormat = NewLineFormat.Dos; + options.EnsureNewLine = true; + + message.WriteTo (options, stream); + length = stream.Length; + stream.Position = 0; + + using (var reader = new StreamReader (stream, Latin1)) + latin1 = reader.ReadToEnd (); + } + + var tag = string.Format ("A{0:D8}", id++); + command.Clear (); + + command.AppendFormat ("{0} {1} {2} INBOX (\\Seen) ", tag, byUid ? "UID REPLACE" : "REPLACE", i + 1); + command.AppendFormat ("ANNOTATION (/altsubject (value.priv \"Alternate subject {0}\")) ", i); + + command.Append ('{').Append (length.ToString (CultureInfo.InvariantCulture)).Append ("+}\r\n").Append (latin1).Append ("\r\n"); + commands.Add (new ImapReplayCommand (command.ToString (), string.Format ("dovecot.append.{0}.txt", i + 1))); + } + + commands.Add (new ImapReplayCommand (string.Format ("A{0:D8} LOGOUT\r\n", id), "gmail.logout.txt")); + + return commands; + } + + [Test] + public void TestReplaceWithAnnotations () + { + var commands = CreateReplaceWithAnnotationsCommands (false, out var requests); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + client.AuthenticationMechanisms.Clear (); + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + client.Inbox.Open (FolderAccess.ReadWrite); + + for (int i = 0; i < requests.Count; i++) { + var uid = client.Inbox.Replace (i, requests[i]); + + Assert.That (uid.HasValue, Is.True, "Expected a UIDAPPEND resp-code"); + Assert.That (uid.Value.Id, Is.EqualTo (i + 1), "Unexpected UID"); + + requests[i].Message.Dispose (); + } + + client.Disconnect (true); + } + } + + [Test] + public async Task TestReplaceWithAnnotationsAsync () + { + var commands = CreateReplaceWithAnnotationsCommands (false, out var requests); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + client.AuthenticationMechanisms.Clear (); + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + await client.Inbox.OpenAsync (FolderAccess.ReadWrite); + + for (int i = 0; i < requests.Count; i++) { + var uid = await client.Inbox.ReplaceAsync (i, requests[i]); + + Assert.That (uid.HasValue, Is.True, "Expected a UIDAPPEND resp-code"); + Assert.That (uid.Value.Id, Is.EqualTo (i + 1), "Unexpected UID"); + + requests[i].Message.Dispose (); + } + + await client.DisconnectAsync (true); + } + } + + [Test] + public void TestReplaceByUidWithAnnotations () + { + var commands = CreateReplaceWithAnnotationsCommands (true, out var requests); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + client.AuthenticationMechanisms.Clear (); + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + client.Inbox.Open (FolderAccess.ReadWrite); + + for (int i = 0; i < requests.Count; i++) { + var uid = client.Inbox.Replace (new UniqueId ((uint) i + 1), requests[i]); + + Assert.That (uid.HasValue, Is.True, "Expected a UIDAPPEND resp-code"); + Assert.That (uid.Value.Id, Is.EqualTo (i + 1), "Unexpected UID"); + + requests[i].Message.Dispose (); + } + + client.Disconnect (true); + } + } + + [Test] + public async Task TestReplaceByUidWithAnnotationsAsync () + { + var commands = CreateReplaceWithAnnotationsCommands (true, out var requests); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + client.AuthenticationMechanisms.Clear (); + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + await client.Inbox.OpenAsync (FolderAccess.ReadWrite); + + for (int i = 0; i < requests.Count; i++) { + var uid = await client.Inbox.ReplaceAsync (new UniqueId ((uint) i + 1), requests[i]); + + Assert.That (uid.HasValue, Is.True, "Expected a UIDAPPEND resp-code"); + Assert.That (uid.Value.Id, Is.EqualTo (i + 1), "Unexpected UID"); + + requests[i].Message.Dispose (); + } + + await client.DisconnectAsync (true); + } + } + + static IList CreateSelectAnnotateNoneCommands () + { + return new List { + new ImapReplayCommand ("", "dovecot.greeting.txt"), + new ImapReplayCommand ("A00000000 LOGIN username password\r\n", "dovecot.authenticate+annotate.txt"), + new ImapReplayCommand ("A00000001 NAMESPACE\r\n", "dovecot.namespace.txt"), + new ImapReplayCommand ("A00000002 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-inbox.txt"), + new ImapReplayCommand ("A00000003 LIST (SPECIAL-USE) \"\" \"*\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-special-use.txt"), + new ImapReplayCommand ("A00000004 SELECT INBOX (CONDSTORE ANNOTATE)\r\n", "common.select-inbox-annotate-none.txt") + }; + } + + [Test] + public void TestSelectAnnotateNone () + { + var commands = CreateSelectAnnotateNoneCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + var credentials = new NetworkCredential ("username", "password"); + + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + client.Authenticate (credentials); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Inbox.SyncRoot, Is.InstanceOf (), "SyncRoot"); + + var inbox = (ImapFolder) client.Inbox; + inbox.Open (FolderAccess.ReadWrite); + + Assert.That (inbox.AnnotationAccess, Is.EqualTo (AnnotationAccess.None), "AnnotationAccess"); + Assert.That (inbox.AnnotationScopes, Is.EqualTo (AnnotationScope.None), "AnnotationScopes"); + Assert.That (inbox.MaxAnnotationSize, Is.EqualTo (0), "MaxAnnotationSize"); + + client.Disconnect (false); + } + } + + [Test] + public async Task TestSelectAnnotateNoneAsync () + { + var commands = CreateSelectAnnotateNoneCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + var credentials = new NetworkCredential ("username", "password"); + + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + await client.AuthenticateAsync (credentials); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Inbox.SyncRoot, Is.InstanceOf (), "SyncRoot"); + + var inbox = (ImapFolder) client.Inbox; + await inbox.OpenAsync (FolderAccess.ReadWrite); + + Assert.That (inbox.AnnotationAccess, Is.EqualTo (AnnotationAccess.None), "AnnotationAccess"); + Assert.That (inbox.AnnotationScopes, Is.EqualTo (AnnotationScope.None), "AnnotationScopes"); + Assert.That (inbox.MaxAnnotationSize, Is.EqualTo (0), "MaxAnnotationSize"); + + await client.DisconnectAsync (false); + } + } + + static List CreateSearchAnnotationsCommands () + { + var commands = new List { + new ImapReplayCommand ("", "dovecot.greeting.txt"), + new ImapReplayCommand ("A00000000 LOGIN username password\r\n", "dovecot.authenticate+annotate.txt"), + new ImapReplayCommand ("A00000001 NAMESPACE\r\n", "dovecot.namespace.txt"), + new ImapReplayCommand ("A00000002 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-inbox.txt"), + new ImapReplayCommand ("A00000003 LIST (SPECIAL-USE) \"\" \"*\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-special-use.txt"), + new ImapReplayCommand ("A00000004 SELECT INBOX (CONDSTORE ANNOTATE)\r\n", "common.select-inbox-annotate-readonly.txt"), + new ImapReplayCommand ("A00000005 UID SEARCH RETURN (ALL) ANNOTATION /comment value \"a comment\"\r\n", "dovecot.search-uids.txt") + }; + + return commands; + } + + [Test] + public void TestSearchAnnotations () + { + var commands = CreateSearchAnnotationsCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + var credentials = new NetworkCredential ("username", "password"); + + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + client.Authenticate (credentials); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Inbox.SyncRoot, Is.InstanceOf (), "SyncRoot"); + + var inbox = (ImapFolder) client.Inbox; + inbox.Open (FolderAccess.ReadWrite); + + Assert.That (inbox.AnnotationAccess, Is.EqualTo (AnnotationAccess.ReadOnly), "AnnotationAccess"); + Assert.That (inbox.AnnotationScopes, Is.EqualTo (AnnotationScope.Both), "AnnotationScopes"); + Assert.That (inbox.MaxAnnotationSize, Is.EqualTo (0), "MaxAnnotationSize"); + + var query = SearchQuery.AnnotationsContain (AnnotationEntry.Comment, AnnotationAttribute.Value, "a comment"); + var uids = inbox.Search (query); + + Assert.That (uids, Has.Count.EqualTo (14), "Unexpected number of UIDs"); + + // disable ANNOTATE-EXPERIMENT-1 and try again + client.Capabilities &= ~ImapCapabilities.Annotate; + + Assert.Throws (() => inbox.Search (query)); + + client.Disconnect (false); + } + } + + [Test] + public async Task TestSearchAnnotationsAsync () + { + var commands = CreateSearchAnnotationsCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + var credentials = new NetworkCredential ("username", "password"); + + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + await client.AuthenticateAsync (credentials); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Inbox.SyncRoot, Is.InstanceOf (), "SyncRoot"); + + var inbox = (ImapFolder) client.Inbox; + await inbox.OpenAsync (FolderAccess.ReadWrite); + + Assert.That (inbox.AnnotationAccess, Is.EqualTo (AnnotationAccess.ReadOnly), "AnnotationAccess"); + Assert.That (inbox.AnnotationScopes, Is.EqualTo (AnnotationScope.Both), "AnnotationScopes"); + Assert.That (inbox.MaxAnnotationSize, Is.EqualTo (0), "MaxAnnotationSize"); + + var query = SearchQuery.AnnotationsContain (AnnotationEntry.Comment, AnnotationAttribute.Value, "a comment"); + var uids = await inbox.SearchAsync (query); + + Assert.That (uids, Has.Count.EqualTo (14), "Unexpected number of UIDs"); + + // disable ANNOTATE-EXPERIMENT-1 and try again + client.Capabilities &= ~ImapCapabilities.Annotate; + + Assert.ThrowsAsync (() => inbox.SearchAsync (query)); + + await client.DisconnectAsync (false); + } + } + + static List CreateSortAnnotationsCommands () + { + return new List { + new ImapReplayCommand ("", "dovecot.greeting.txt"), + new ImapReplayCommand ("A00000000 LOGIN username password\r\n", "dovecot.authenticate+annotate.txt"), + new ImapReplayCommand ("A00000001 NAMESPACE\r\n", "dovecot.namespace.txt"), + new ImapReplayCommand ("A00000002 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-inbox.txt"), + new ImapReplayCommand ("A00000003 LIST (SPECIAL-USE) \"\" \"*\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-special-use.txt"), + new ImapReplayCommand ("A00000004 SELECT INBOX (CONDSTORE ANNOTATE)\r\n", "common.select-inbox-annotate-readonly.txt"), + new ImapReplayCommand ("A00000005 UID SORT RETURN (ALL) (ANNOTATION /altsubject value.shared) US-ASCII ALL\r\n", "dovecot.sort-by-strings.txt"), + new ImapReplayCommand ("A00000006 UID SORT RETURN (ALL) (REVERSE ANNOTATION /altsubject value.shared) US-ASCII ALL\r\n", "dovecot.sort-by-strings.txt") + }; + } + + [Test] + public void TestSortAnnotations () + { + var commands = CreateSortAnnotationsCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + var credentials = new NetworkCredential ("username", "password"); + + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + client.Authenticate (credentials); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Inbox.SyncRoot, Is.InstanceOf (), "SyncRoot"); + + var inbox = (ImapFolder) client.Inbox; + inbox.Open (FolderAccess.ReadWrite); + + Assert.That (inbox.AnnotationAccess, Is.EqualTo (AnnotationAccess.ReadOnly), "AnnotationAccess"); + Assert.That (inbox.AnnotationScopes, Is.EqualTo (AnnotationScope.Both), "AnnotationScopes"); + Assert.That (inbox.MaxAnnotationSize, Is.EqualTo (0), "MaxAnnotationSize"); + + var orderBy = new OrderByAnnotation (AnnotationEntry.AltSubject, AnnotationAttribute.SharedValue, SortOrder.Ascending); + var uids = inbox.Sort (SearchQuery.All, new OrderBy[] { orderBy }); + + Assert.That (uids, Has.Count.EqualTo (14), "Unexpected number of UIDs"); + + orderBy = new OrderByAnnotation (AnnotationEntry.AltSubject, AnnotationAttribute.SharedValue, SortOrder.Descending); + uids = inbox.Sort (SearchQuery.All, new OrderBy[] { orderBy }); + + Assert.That (uids, Has.Count.EqualTo (14), "Unexpected number of UIDs"); + + // disable ANNOTATE-EXPERIMENT-1 and try again + client.Capabilities &= ~ImapCapabilities.Annotate; + + Assert.Throws (() => inbox.Sort (SearchQuery.All, new OrderBy[] { orderBy })); + + client.Disconnect (false); + } + } + + [Test] + public async Task TestSortAnnotationsAsync () + { + var commands = CreateSortAnnotationsCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + var credentials = new NetworkCredential ("username", "password"); + + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + await client.AuthenticateAsync (credentials); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Inbox.SyncRoot, Is.InstanceOf (), "SyncRoot"); + + var inbox = (ImapFolder) client.Inbox; + await inbox.OpenAsync (FolderAccess.ReadWrite); + + Assert.That (inbox.AnnotationAccess, Is.EqualTo (AnnotationAccess.ReadOnly), "AnnotationAccess"); + Assert.That (inbox.AnnotationScopes, Is.EqualTo (AnnotationScope.Both), "AnnotationScopes"); + Assert.That (inbox.MaxAnnotationSize, Is.EqualTo (0), "MaxAnnotationSize"); + + var orderBy = new OrderByAnnotation (AnnotationEntry.AltSubject, AnnotationAttribute.SharedValue, SortOrder.Ascending); + var uids = await inbox.SortAsync (SearchQuery.All, new OrderBy[] { orderBy }); + + Assert.That (uids, Has.Count.EqualTo (14), "Unexpected number of UIDs"); + + orderBy = new OrderByAnnotation (AnnotationEntry.AltSubject, AnnotationAttribute.SharedValue, SortOrder.Descending); + uids = await inbox.SortAsync (SearchQuery.All, new OrderBy[] { orderBy }); + + Assert.That (uids, Has.Count.EqualTo (14), "Unexpected number of UIDs"); + + // disable ANNOTATE-EXPERIMENT-1 and try again + client.Capabilities &= ~ImapCapabilities.Annotate; + + Assert.ThrowsAsync (() => inbox.SortAsync (SearchQuery.All, new OrderBy[] { orderBy })); + + await client.DisconnectAsync (false); + } + } + + static List CreateStoreCommands () + { + return new List { + new ImapReplayCommand ("", "dovecot.greeting.txt"), + new ImapReplayCommand ("A00000000 LOGIN username password\r\n", "dovecot.authenticate+annotate.txt"), + new ImapReplayCommand ("A00000001 NAMESPACE\r\n", "dovecot.namespace.txt"), + new ImapReplayCommand ("A00000002 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-inbox.txt"), + new ImapReplayCommand ("A00000003 LIST (SPECIAL-USE) \"\" \"*\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-special-use.txt"), + new ImapReplayCommand ("A00000004 SELECT INBOX (CONDSTORE ANNOTATE)\r\n", "common.select-inbox-annotate.txt"), + new ImapReplayCommand ("A00000005 STORE 1 ANNOTATION (/altsubject (value.shared \"This is an alternate subject.\"))\r\n", ImapReplayCommandResponse.OK), + new ImapReplayCommand ("A00000006 UID STORE 1 ANNOTATION (/altsubject (value.shared \"This is an alternate subject.\"))\r\n", ImapReplayCommandResponse.OK), + new ImapReplayCommand ("A00000007 STORE 1 ANNOTATION (/altsubject (value.shared NIL))\r\n", ImapReplayCommandResponse.OK), + new ImapReplayCommand ("A00000008 UID STORE 1 ANNOTATION (/altsubject (value.shared NIL))\r\n", ImapReplayCommandResponse.OK), + new ImapReplayCommand ("A00000009 STORE 1 (UNCHANGEDSINCE 42) ANNOTATION (/altsubject (value.shared NIL))\r\n", ImapReplayCommandResponse.OK), + new ImapReplayCommand ("A00000010 UID STORE 1 (UNCHANGEDSINCE 42) ANNOTATION (/altsubject (value.shared NIL))\r\n", ImapReplayCommandResponse.OK), + new ImapReplayCommand ("A00000011 STORE 1 ANNOTATION (/altsubject (value.shared \"This alternate subject will cause an error.\"))\r\n", Encoding.ASCII.GetBytes ("A00000011 NO [ANNOTATE TOOBIG] Annotate failed.\r\n")), + new ImapReplayCommand ("A00000012 UID STORE 1 ANNOTATION (/altsubject (value.shared \"This alternate subject will cause an error.\"))\r\n", Encoding.ASCII.GetBytes ("A00000012 NO [ANNOTATE TOOMANY] Annotate failed.\r\n")), + }; + } + + [Test] + public void TestStore () + { + var commands = CreateStoreCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + var credentials = new NetworkCredential ("username", "password"); + + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + client.Authenticate (credentials); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Inbox.SyncRoot, Is.InstanceOf (), "SyncRoot"); + + var inbox = (ImapFolder) client.Inbox; + inbox.Open (FolderAccess.ReadWrite); + + Assert.That (inbox.AnnotationAccess, Is.EqualTo (AnnotationAccess.ReadWrite), "AnnotationAccess"); + Assert.That (inbox.AnnotationScopes, Is.EqualTo (AnnotationScope.Shared), "AnnotationScopes"); + Assert.That (inbox.MaxAnnotationSize, Is.EqualTo (20480), "MaxAnnotationSize"); + + var annotation = new Annotation (AnnotationEntry.AltSubject); + annotation.Properties.Add (AnnotationAttribute.SharedValue, "This is an alternate subject."); + + var annotations = new [] { annotation }; + + inbox.Store (0, annotations); + inbox.Store (new UniqueId (1), annotations); + + annotation.Properties[AnnotationAttribute.SharedValue] = null; + + inbox.Store (0, annotations); + inbox.Store (new UniqueId (1), annotations); + + inbox.Store (new[] { 0 }, 42, annotations); + inbox.Store (new[] { new UniqueId (1) }, 42, annotations); + + annotation = new Annotation (AnnotationEntry.AltSubject); + annotation.Properties.Add (AnnotationAttribute.SharedValue, "This alternate subject will cause an error."); + + annotations = new[] { annotation }; + + Assert.Throws (() => inbox.Store (0, annotations)); + Assert.Throws (() => inbox.Store (new UniqueId (1), annotations)); + + client.Disconnect (false); + } + } + + [Test] + public async Task TestStoreAsync () + { + var commands = CreateStoreCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + var credentials = new NetworkCredential ("username", "password"); + + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + await client.AuthenticateAsync (credentials); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Inbox.SyncRoot, Is.InstanceOf (), "SyncRoot"); + + var inbox = (ImapFolder) client.Inbox; + await inbox.OpenAsync (FolderAccess.ReadWrite); + + Assert.That (inbox.AnnotationAccess, Is.EqualTo (AnnotationAccess.ReadWrite), "AnnotationAccess"); + Assert.That (inbox.AnnotationScopes, Is.EqualTo (AnnotationScope.Shared), "AnnotationScopes"); + Assert.That (inbox.MaxAnnotationSize, Is.EqualTo (20480), "MaxAnnotationSize"); + + var annotation = new Annotation (AnnotationEntry.AltSubject); + annotation.Properties.Add (AnnotationAttribute.SharedValue, "This is an alternate subject."); + + var annotations = new[] { annotation }; + + await inbox.StoreAsync (0, annotations); + await inbox.StoreAsync (new UniqueId (1), annotations); + + annotation.Properties[AnnotationAttribute.SharedValue] = null; + + await inbox.StoreAsync (0, annotations); + await inbox.StoreAsync (new UniqueId (1), annotations); + + await inbox.StoreAsync (new[] { 0 }, 42, annotations); + await inbox.StoreAsync (new[] { new UniqueId (1) }, 42, annotations); + + annotation = new Annotation (AnnotationEntry.AltSubject); + annotation.Properties.Add (AnnotationAttribute.SharedValue, "This alternate subject will cause an error."); + + annotations = new[] { annotation }; + + Assert.ThrowsAsync (() => inbox.StoreAsync (0, annotations)); + Assert.ThrowsAsync (() => inbox.StoreAsync (new UniqueId (1), annotations)); + + await client.DisconnectAsync (false); + } + } + } +} diff --git a/UnitTests/Net/Imap/ImapFolderFetchTests.cs b/UnitTests/Net/Imap/ImapFolderFetchTests.cs new file mode 100644 index 0000000000..232679f310 --- /dev/null +++ b/UnitTests/Net/Imap/ImapFolderFetchTests.cs @@ -0,0 +1,2822 @@ +// +// ImapFolderFetchTests.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System.Net; +using System.Text; +using System.Security.Cryptography; + +using MimeKit; + +using MailKit; +using MailKit.Security; +using MailKit.Net.Imap; + +namespace UnitTests.Net.Imap { + [TestFixture] + public class ImapFolderFetchTests + { + static FolderAttributes GetSpecialFolderAttribute (SpecialFolder special) + { + switch (special) { + case SpecialFolder.All: return FolderAttributes.All; + case SpecialFolder.Archive: return FolderAttributes.Archive; + case SpecialFolder.Drafts: return FolderAttributes.Drafts; + case SpecialFolder.Flagged: return FolderAttributes.Flagged; + case SpecialFolder.Important: return FolderAttributes.Important; + case SpecialFolder.Junk: return FolderAttributes.Junk; + case SpecialFolder.Sent: return FolderAttributes.Sent; + case SpecialFolder.Trash: return FolderAttributes.Trash; + default: throw new ArgumentOutOfRangeException (nameof (special)); + } + } + + static string HexEncode (byte [] digest) + { + var hex = new StringBuilder (); + + for (int i = 0; i < digest.Length; i++) + hex.Append (digest[i].ToString ("x2")); + + return hex.ToString (); + } + + static void GetStreamsCallback (ImapFolder folder, int index, UniqueId uid, Stream stream) + { + using (var reader = new StreamReader (stream)) { + const string expected = "This is some dummy text just to make sure this is working correctly."; + var text = reader.ReadToEnd (); + + Assert.That (text, Is.EqualTo (expected)); + } + } + + static async Task GetStreamsAsyncCallback (ImapFolder folder, int index, UniqueId uid, Stream stream, CancellationToken cancellationToken) + { + using (var reader = new StreamReader (stream)) { + const string expected = "This is some dummy text just to make sure this is working correctly."; + var text = await reader.ReadToEndAsync (); + + Assert.That (text, Is.EqualTo (expected)); + } + } + + [Test] + public void TestArgumentExceptions () + { + var commands = new List { + new ImapReplayCommand ("", "dovecot.greeting.txt"), + new ImapReplayCommand ("A00000000 LOGIN username password\r\n", "dovecot.authenticate+gmail-capabilities.txt"), + new ImapReplayCommand ("A00000001 NAMESPACE\r\n", "dovecot.namespace.txt"), + new ImapReplayCommand ("A00000002 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-inbox.txt"), + new ImapReplayCommand ("A00000003 LIST (SPECIAL-USE) \"\" \"*\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-special-use.txt"), + new ImapReplayCommand ("A00000004 SELECT INBOX (CONDSTORE)\r\n", "common.select-inbox.txt") + }; + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + var credentials = new NetworkCredential ("username", "password"); + + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + client.Authenticate (credentials); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Inbox.SyncRoot, Is.InstanceOf (), "SyncRoot"); + + var inbox = (ImapFolder) client.Inbox; + inbox.Open (FolderAccess.ReadWrite); + + // Fetch + var invalidHeaderFields = new string[] { "Invalid Header Name" }; + var headerIds = new HeaderId [] { HeaderId.Subject }; + var headerFields = new string [] { "SUBJECT" }; + var uids = new UniqueId [] { UniqueId.MinValue }; + var indexes = new int [] { 0 }; + + Assert.Throws (() => inbox.Fetch (-1, -1, MessageSummaryItems.All)); + Assert.ThrowsAsync (async () => await inbox.FetchAsync (-1, -1, MessageSummaryItems.All)); + Assert.Throws (() => inbox.Fetch (5, 1, MessageSummaryItems.All)); + Assert.ThrowsAsync (async () => await inbox.FetchAsync (5, 1, MessageSummaryItems.All)); + Assert.Throws (() => inbox.Fetch (0, -1, null)); + Assert.ThrowsAsync (() => inbox.FetchAsync (0, -1, null)); + + Assert.Throws (() => inbox.Fetch ((IList) null, MessageSummaryItems.All)); + Assert.ThrowsAsync (async () => await inbox.FetchAsync ((IList) null, MessageSummaryItems.All)); + Assert.Throws (() => inbox.Fetch (uids, null)); + Assert.ThrowsAsync (() => inbox.FetchAsync (uids, null)); + + Assert.Throws (() => inbox.Fetch ((IList) null, MessageSummaryItems.All)); + Assert.ThrowsAsync (async () => await inbox.FetchAsync ((IList) null, MessageSummaryItems.All)); + Assert.Throws (() => inbox.Fetch (indexes, null)); + Assert.ThrowsAsync (() => inbox.FetchAsync (indexes, null)); + + Assert.Throws (() => inbox.Fetch (-1, -1, MessageSummaryItems.All, headerIds)); + Assert.ThrowsAsync (async () => await inbox.FetchAsync (-1, -1, MessageSummaryItems.All, headerIds)); + Assert.Throws (() => inbox.Fetch (5, 1, MessageSummaryItems.All, headerIds)); + Assert.ThrowsAsync (async () => await inbox.FetchAsync (5, 1, MessageSummaryItems.All, headerIds)); + //Assert.Throws (() => inbox.Fetch (0, 5, MessageSummaryItems.None, headers)); + //Assert.ThrowsAsync (async () => await inbox.FetchAsync (0, 5, MessageSummaryItems.None, headers)); + Assert.Throws (() => inbox.Fetch (0, 5, MessageSummaryItems.All, (HashSet) null)); + Assert.ThrowsAsync (async () => await inbox.FetchAsync (0, 5, MessageSummaryItems.All, (HashSet) null)); + + Assert.Throws (() => inbox.Fetch ((IList) null, MessageSummaryItems.All, headerIds)); + Assert.ThrowsAsync (async () => await inbox.FetchAsync ((IList) null, MessageSummaryItems.All, headerIds)); + //Assert.Throws (() => inbox.Fetch (uids, MessageSummaryItems.None, headers)); + //Assert.ThrowsAsync (async () => await inbox.FetchAsync (uids, MessageSummaryItems.None, headers)); + Assert.Throws (() => inbox.Fetch (uids, MessageSummaryItems.All, (HashSet) null)); + Assert.ThrowsAsync (async () => await inbox.FetchAsync (uids, MessageSummaryItems.All, (HashSet) null)); + + Assert.Throws (() => inbox.Fetch ((IList) null, MessageSummaryItems.All, headerIds)); + Assert.ThrowsAsync (async () => await inbox.FetchAsync ((IList) null, MessageSummaryItems.All, headerIds)); + //Assert.Throws (() => inbox.Fetch (indexes, MessageSummaryItems.None, headers)); + //Assert.ThrowsAsync (async () => await inbox.FetchAsync (indexes, MessageSummaryItems.None, headers)); + Assert.Throws (() => inbox.Fetch (indexes, MessageSummaryItems.All, (HashSet) null)); + Assert.ThrowsAsync (async () => await inbox.FetchAsync (indexes, MessageSummaryItems.All, (HashSet) null)); + + Assert.Throws (() => inbox.Fetch (-1, -1, MessageSummaryItems.All, headerFields)); + Assert.ThrowsAsync (async () => await inbox.FetchAsync (-1, -1, MessageSummaryItems.All, headerFields)); + Assert.Throws (() => inbox.Fetch (5, 1, MessageSummaryItems.All, headerFields)); + Assert.ThrowsAsync (async () => await inbox.FetchAsync (5, 1, MessageSummaryItems.All, headerFields)); + //Assert.Throws (() => inbox.Fetch (0, 5, MessageSummaryItems.None, fields)); + //Assert.ThrowsAsync (async () => await inbox.FetchAsync (0, 5, MessageSummaryItems.None, fields)); + Assert.Throws (() => inbox.Fetch (0, 5, MessageSummaryItems.All, (HashSet) null)); + Assert.ThrowsAsync (async () => await inbox.FetchAsync (0, 5, MessageSummaryItems.All, (HashSet) null)); + Assert.Throws (() => inbox.Fetch (0, 5, MessageSummaryItems.All, invalidHeaderFields)); + Assert.ThrowsAsync (async () => await inbox.FetchAsync (0, 5, MessageSummaryItems.All, invalidHeaderFields)); + + Assert.Throws (() => inbox.Fetch ((IList) null, MessageSummaryItems.All, headerFields)); + Assert.ThrowsAsync (async () => await inbox.FetchAsync ((IList) null, MessageSummaryItems.All, headerFields)); + //Assert.Throws (() => inbox.Fetch (uids, MessageSummaryItems.None, fields)); + //Assert.ThrowsAsync (async () => await inbox.FetchAsync (uids, MessageSummaryItems.None, fields)); + Assert.Throws (() => inbox.Fetch (uids, MessageSummaryItems.All, (HashSet) null)); + Assert.ThrowsAsync (async () => await inbox.FetchAsync (uids, MessageSummaryItems.All, (HashSet) null)); + Assert.Throws (() => inbox.Fetch (uids, MessageSummaryItems.All, invalidHeaderFields)); + Assert.ThrowsAsync (async () => await inbox.FetchAsync (uids, MessageSummaryItems.All, invalidHeaderFields)); + + Assert.Throws (() => inbox.Fetch ((IList) null, MessageSummaryItems.All, headerFields)); + Assert.ThrowsAsync (async () => await inbox.FetchAsync ((IList) null, MessageSummaryItems.All, headerFields)); + //Assert.Throws (() => inbox.Fetch (indexes, MessageSummaryItems.None, fields)); + //Assert.ThrowsAsync (async () => await inbox.FetchAsync (indexes, MessageSummaryItems.None, fields)); + Assert.Throws (() => inbox.Fetch (indexes, MessageSummaryItems.All, (HashSet) null)); + Assert.ThrowsAsync (async () => await inbox.FetchAsync (indexes, MessageSummaryItems.All, (HashSet) null)); + Assert.Throws (() => inbox.Fetch (indexes, MessageSummaryItems.All, invalidHeaderFields)); + Assert.ThrowsAsync (async () => await inbox.FetchAsync (indexes, MessageSummaryItems.All, invalidHeaderFields)); + + // Fetch + modseq + Assert.Throws (() => inbox.Fetch (-1, -1, 31337, MessageSummaryItems.All)); + Assert.ThrowsAsync (async () => await inbox.FetchAsync (-1, -1, 31337, MessageSummaryItems.All)); + Assert.Throws (() => inbox.Fetch (5, 1, 31337, MessageSummaryItems.All)); + Assert.ThrowsAsync (async () => await inbox.FetchAsync (5, 1, 31337, MessageSummaryItems.All)); + + Assert.Throws (() => inbox.Fetch ((IList) null, 31337, MessageSummaryItems.All)); + Assert.ThrowsAsync (async () => await inbox.FetchAsync ((IList) null, 31337, MessageSummaryItems.All)); + + Assert.Throws (() => inbox.Fetch ((IList) null, 31337, MessageSummaryItems.All)); + Assert.ThrowsAsync (async () => await inbox.FetchAsync ((IList) null, 31337, MessageSummaryItems.All)); + + Assert.Throws (() => inbox.Fetch (-1, -1, 31337, MessageSummaryItems.All, headerIds)); + Assert.ThrowsAsync (async () => await inbox.FetchAsync (-1, -1, 31337, MessageSummaryItems.All, headerIds)); + Assert.Throws (() => inbox.Fetch (5, 1, 31337, MessageSummaryItems.All, headerIds)); + Assert.ThrowsAsync (async () => await inbox.FetchAsync (5, 1, MessageSummaryItems.All, headerIds)); + Assert.Throws (() => inbox.Fetch (0, 5, 31337, MessageSummaryItems.All, (HashSet) null)); + Assert.ThrowsAsync (async () => await inbox.FetchAsync (0, 5, 31337, MessageSummaryItems.All, (HashSet) null)); + + Assert.Throws (() => inbox.Fetch ((IList) null, 31337, MessageSummaryItems.All, headerIds)); + Assert.ThrowsAsync (async () => await inbox.FetchAsync ((IList) null, 31337, MessageSummaryItems.All, headerIds)); + Assert.Throws (() => inbox.Fetch (uids, 31337, MessageSummaryItems.All, (HashSet) null)); + Assert.ThrowsAsync (async () => await inbox.FetchAsync (uids, 31337, MessageSummaryItems.All, (HashSet) null)); + + Assert.Throws (() => inbox.Fetch ((IList) null, 31337, MessageSummaryItems.All, headerIds)); + Assert.ThrowsAsync (async () => await inbox.FetchAsync ((IList) null, 31337, MessageSummaryItems.All, headerIds)); + Assert.Throws (() => inbox.Fetch (indexes, 31337, MessageSummaryItems.All, (HashSet) null)); + Assert.ThrowsAsync (async () => await inbox.FetchAsync (indexes, 31337, MessageSummaryItems.All, (HashSet) null)); + + Assert.Throws (() => inbox.Fetch (-1, -1, 31337, MessageSummaryItems.All, headerFields)); + Assert.ThrowsAsync (async () => await inbox.FetchAsync (-1, -1, 31337, MessageSummaryItems.All, headerFields)); + Assert.Throws (() => inbox.Fetch (5, 1, 31337, MessageSummaryItems.All, headerFields)); + Assert.ThrowsAsync (async () => await inbox.FetchAsync (5, 1, 31337, MessageSummaryItems.All, headerFields)); + Assert.Throws (() => inbox.Fetch (0, 5, 31337, MessageSummaryItems.All, (HashSet) null)); + Assert.ThrowsAsync (async () => await inbox.FetchAsync (0, 5, 31337, MessageSummaryItems.All, (HashSet) null)); + Assert.Throws (() => inbox.Fetch (0, 5, 31337, MessageSummaryItems.All, invalidHeaderFields)); + Assert.ThrowsAsync (async () => await inbox.FetchAsync (0, 5, 31337, MessageSummaryItems.All, invalidHeaderFields)); + + Assert.Throws (() => inbox.Fetch ((IList) null, 31337, MessageSummaryItems.All, headerFields)); + Assert.ThrowsAsync (async () => await inbox.FetchAsync ((IList) null, 31337, MessageSummaryItems.All, headerFields)); + Assert.Throws (() => inbox.Fetch (uids, 31337, MessageSummaryItems.All, (HashSet) null)); + Assert.ThrowsAsync (async () => await inbox.FetchAsync (uids, 31337, MessageSummaryItems.All, (HashSet) null)); + Assert.Throws (() => inbox.Fetch (uids, 31337, MessageSummaryItems.All, invalidHeaderFields)); + Assert.ThrowsAsync (async () => await inbox.FetchAsync (uids, 31337, MessageSummaryItems.All, invalidHeaderFields)); + + Assert.Throws (() => inbox.Fetch ((IList) null, 31337, MessageSummaryItems.All, headerFields)); + Assert.ThrowsAsync (async () => await inbox.FetchAsync ((IList) null, 31337, MessageSummaryItems.All, headerFields)); + Assert.Throws (() => inbox.Fetch (indexes, 31337, MessageSummaryItems.All, (HashSet) null)); + Assert.ThrowsAsync (async () => await inbox.FetchAsync (indexes, 31337, MessageSummaryItems.All, (HashSet) null)); + Assert.Throws (() => inbox.Fetch (indexes, 31337, MessageSummaryItems.All, invalidHeaderFields)); + Assert.ThrowsAsync (async () => await inbox.FetchAsync (indexes, 31337, MessageSummaryItems.All, invalidHeaderFields)); + + // GetHeaders + Assert.Throws (() => inbox.GetHeaders (-1)); + Assert.ThrowsAsync (async () => await inbox.GetHeadersAsync (-1)); + Assert.Throws (() => inbox.GetHeaders (UniqueId.Invalid)); + Assert.ThrowsAsync (async () => await inbox.GetHeadersAsync (UniqueId.Invalid)); + + var bodyPart = new BodyPartText (new ContentType ("text", "plain"), "1.2"); + + Assert.Throws (() => inbox.GetHeaders (-1, bodyPart)); + Assert.ThrowsAsync (async () => await inbox.GetHeadersAsync (-1, bodyPart)); + Assert.Throws (() => inbox.GetHeaders (0, (BodyPart) null)); + Assert.ThrowsAsync (async () => await inbox.GetHeadersAsync (0, (BodyPart) null)); + + Assert.Throws (() => inbox.GetHeaders (UniqueId.Invalid, bodyPart)); + Assert.ThrowsAsync (async () => await inbox.GetHeadersAsync (UniqueId.Invalid, bodyPart)); + Assert.Throws (() => inbox.GetHeaders (UniqueId.MinValue, (BodyPart) null)); + Assert.ThrowsAsync (async () => await inbox.GetHeadersAsync (UniqueId.MinValue, (BodyPart) null)); + + Assert.Throws (() => inbox.GetHeaders (-1, "1.2")); + //Assert.ThrowsAsync (async () => await inbox.GetHeadersAsync (-1, "1.2")); + Assert.Throws (() => inbox.GetHeaders (0, (string) null)); + //Assert.ThrowsAsync (async () => await inbox.GetHeadersAsync (0, (string) null)); + + Assert.Throws (() => inbox.GetHeaders (UniqueId.Invalid, "1.2")); + //Assert.ThrowsAsync (async () => await inbox.GetHeadersAsync (UniqueId.Invalid, "1.2")); + Assert.Throws (() => inbox.GetHeaders (UniqueId.MinValue, (string) null)); + //Assert.ThrowsAsync (async () => await inbox.GetHeadersAsync (UniqueId.MinValue, (string) null)); + + // GetMessage + Assert.Throws (() => inbox.GetMessage (-1)); + Assert.ThrowsAsync (async () => await inbox.GetMessageAsync (-1)); + Assert.Throws (() => inbox.GetMessage (UniqueId.Invalid)); + Assert.ThrowsAsync (async () => await inbox.GetMessageAsync (UniqueId.Invalid)); + + // GetBodyPart + Assert.Throws (() => inbox.GetBodyPart (-1, bodyPart)); + Assert.ThrowsAsync (async () => await inbox.GetBodyPartAsync (-1, bodyPart)); + Assert.Throws (() => inbox.GetBodyPart (0, (BodyPart) null)); + Assert.ThrowsAsync (async () => await inbox.GetBodyPartAsync (0, (BodyPart) null)); + + Assert.Throws (() => inbox.GetBodyPart (UniqueId.Invalid, bodyPart)); + Assert.ThrowsAsync (async () => await inbox.GetBodyPartAsync (UniqueId.Invalid, bodyPart)); + Assert.Throws (() => inbox.GetBodyPart (UniqueId.MinValue, (BodyPart) null)); + Assert.ThrowsAsync (async () => await inbox.GetBodyPartAsync (UniqueId.MinValue, (BodyPart) null)); + + Assert.Throws (() => inbox.GetBodyPart (-1, "1.2")); + //Assert.ThrowsAsync (async () => await inbox.GetBodyPartAsync (-1, "1.2")); + Assert.Throws (() => inbox.GetBodyPart (0, (string) null)); + //Assert.ThrowsAsync (async () => await inbox.GetBodyPartAsync (0, (string) null)); + + Assert.Throws (() => inbox.GetBodyPart (UniqueId.Invalid, "1.2")); + //Assert.ThrowsAsync (async () => await inbox.GetBodyPartAsync (UniqueId.Invalid, "1.2")); + Assert.Throws (() => inbox.GetBodyPart (UniqueId.MinValue, (string) null)); + //Assert.ThrowsAsync (async () => await inbox.GetBodyPartAsync (UniqueId.MinValue, (string) null)); + + // GetStream + Assert.Throws (() => inbox.GetStream (-1)); + Assert.ThrowsAsync (async () => await inbox.GetStreamAsync (-1)); + + Assert.Throws (() => inbox.GetStream (UniqueId.Invalid)); + Assert.ThrowsAsync (async () => await inbox.GetStreamAsync (UniqueId.Invalid)); + + Assert.Throws (() => inbox.GetStream (-1, "1.2")); + Assert.ThrowsAsync (async () => await inbox.GetStreamAsync (-1, "1.2")); + Assert.Throws (() => inbox.GetStream (0, (string) null)); + Assert.ThrowsAsync (async () => await inbox.GetStreamAsync (0, (string) null)); + + Assert.Throws (() => inbox.GetStream (UniqueId.Invalid, "1.2")); + Assert.ThrowsAsync (async () => await inbox.GetStreamAsync (UniqueId.Invalid, "1.2")); + Assert.Throws (() => inbox.GetStream (UniqueId.MinValue, (string) null)); + Assert.ThrowsAsync (async () => await inbox.GetStreamAsync (UniqueId.MinValue, (string) null)); + + Assert.Throws (() => inbox.GetStream (-1, bodyPart)); + Assert.ThrowsAsync (async () => await inbox.GetStreamAsync (-1, bodyPart)); + Assert.Throws (() => inbox.GetStream (0, (BodyPart) null)); + Assert.ThrowsAsync (async () => await inbox.GetStreamAsync (0, (BodyPart) null)); + + Assert.Throws (() => inbox.GetStream (UniqueId.Invalid, bodyPart)); + Assert.ThrowsAsync (async () => await inbox.GetStreamAsync (UniqueId.Invalid, bodyPart)); + Assert.Throws (() => inbox.GetStream (UniqueId.MinValue, (BodyPart) null)); + Assert.ThrowsAsync (async () => await inbox.GetStreamAsync (UniqueId.MinValue, (BodyPart) null)); + + Assert.Throws (() => inbox.GetStream (-1, 0, 1024)); + Assert.ThrowsAsync (async () => await inbox.GetStreamAsync (-1, 0, 1024)); + Assert.Throws (() => inbox.GetStream (0, -1, 1024)); + Assert.ThrowsAsync (async () => await inbox.GetStreamAsync (0, -1, 1024)); + Assert.Throws (() => inbox.GetStream (0, 0, -1)); + Assert.ThrowsAsync (async () => await inbox.GetStreamAsync (0, 0, -1)); + + Assert.Throws (() => inbox.GetStream (UniqueId.Invalid, 0, 1024)); + Assert.ThrowsAsync (async () => await inbox.GetStreamAsync (UniqueId.Invalid, 0, 1024)); + Assert.Throws (() => inbox.GetStream (UniqueId.MinValue, -1, 1024)); + Assert.ThrowsAsync (async () => await inbox.GetStreamAsync (UniqueId.MinValue, -1, 1024)); + Assert.Throws (() => inbox.GetStream (UniqueId.MinValue, 0, -1)); + Assert.ThrowsAsync (async () => await inbox.GetStreamAsync (UniqueId.MinValue, 0, -1)); + + Assert.Throws (() => inbox.GetStream (-1, "1.2", 0, 1024)); + Assert.ThrowsAsync (async () => await inbox.GetStreamAsync (-1, "1.2", 0, 1024)); + Assert.Throws (() => inbox.GetStream (0, (string) null, 0, 1024)); + Assert.ThrowsAsync (async () => await inbox.GetStreamAsync (0, (string) null, 0, 1024)); + Assert.Throws (() => inbox.GetStream (0, "1.2", -1, 1024)); + Assert.ThrowsAsync (async () => await inbox.GetStreamAsync (0, "1.2", -1, 1024)); + Assert.Throws (() => inbox.GetStream (0, "1.2", 0, -1)); + Assert.ThrowsAsync (async () => await inbox.GetStreamAsync (0, "1.2", 0, -1)); + + Assert.Throws (() => inbox.GetStream (UniqueId.Invalid, "1.2", 0, 1024)); + Assert.ThrowsAsync (async () => await inbox.GetStreamAsync (UniqueId.Invalid, "1.2", 0, 1024)); + Assert.Throws (() => inbox.GetStream (UniqueId.MinValue, (string) null, 0, 1024)); + Assert.ThrowsAsync (async () => await inbox.GetStreamAsync (UniqueId.MinValue, (string) null, 0, 1024)); + Assert.Throws (() => inbox.GetStream (UniqueId.MinValue, "1.2", -1, 1024)); + Assert.ThrowsAsync (async () => await inbox.GetStreamAsync (UniqueId.MinValue, "1.2", -1, 1024)); + Assert.Throws (() => inbox.GetStream (UniqueId.MinValue, "1.2", 0, -1)); + Assert.ThrowsAsync (async () => await inbox.GetStreamAsync (UniqueId.MinValue, "1.2", 0, -1)); + + Assert.Throws (() => inbox.GetStream (-1, bodyPart, 0, 1024)); + Assert.ThrowsAsync (async () => await inbox.GetStreamAsync (-1, bodyPart, 0, 1024)); + Assert.Throws (() => inbox.GetStream (0, (BodyPart) null, -1, 1024)); + Assert.ThrowsAsync (async () => await inbox.GetStreamAsync (0, (BodyPart) null, -1, 1024)); + Assert.Throws (() => inbox.GetStream (0, bodyPart, -1, 1024)); + Assert.ThrowsAsync (async () => await inbox.GetStreamAsync (0, bodyPart, -1, 1024)); + Assert.Throws (() => inbox.GetStream (0, bodyPart, 0, -1)); + Assert.ThrowsAsync (async () => await inbox.GetStreamAsync (0, bodyPart, 0, -1)); + + Assert.Throws (() => inbox.GetStream (UniqueId.Invalid, bodyPart, 0, 1024)); + Assert.ThrowsAsync (async () => await inbox.GetStreamAsync (UniqueId.Invalid, bodyPart, 0, 1024)); + Assert.Throws (() => inbox.GetStream (UniqueId.MinValue, (BodyPart) null, -1, 1024)); + Assert.ThrowsAsync (async () => await inbox.GetStreamAsync (UniqueId.MinValue, (BodyPart) null, -1, 1024)); + Assert.Throws (() => inbox.GetStream (UniqueId.MinValue, bodyPart, -1, 1024)); + Assert.ThrowsAsync (async () => await inbox.GetStreamAsync (UniqueId.MinValue, bodyPart, -1, 1024)); + Assert.Throws (() => inbox.GetStream (UniqueId.MinValue, bodyPart, 0, -1)); + Assert.ThrowsAsync (async () => await inbox.GetStreamAsync (UniqueId.MinValue, bodyPart, 0, -1)); + + // GetStreams + Assert.Throws (() => inbox.GetStreams (-1, 0, GetStreamsCallback)); + Assert.ThrowsAsync (async () => await inbox.GetStreamsAsync (-1, 0, GetStreamsAsyncCallback)); + Assert.Throws (() => inbox.GetStreams (1, 0, GetStreamsCallback)); + Assert.ThrowsAsync (async () => await inbox.GetStreamsAsync (1, 0, GetStreamsAsyncCallback)); + Assert.Throws (() => inbox.GetStreams (0, -1, null)); + Assert.ThrowsAsync (async () => await inbox.GetStreamsAsync (0, -1, null)); + + Assert.Throws (() => inbox.GetStreams ((IList) null, GetStreamsCallback)); + Assert.ThrowsAsync (async () => await inbox.GetStreamsAsync ((IList) null, GetStreamsAsyncCallback)); + Assert.Throws (() => inbox.GetStreams (new int [] { 0 }, null)); + Assert.ThrowsAsync (async () => await inbox.GetStreamsAsync (new int [] { 0 }, null)); + + Assert.Throws (() => inbox.GetStreams ((IList) null, GetStreamsCallback)); + Assert.ThrowsAsync (async () => await inbox.GetStreamsAsync ((IList) null, GetStreamsAsyncCallback)); + Assert.Throws (() => inbox.GetStreams (UniqueIdRange.All, null)); + Assert.ThrowsAsync (async () => await inbox.GetStreamsAsync (UniqueIdRange.All, null)); + + client.Disconnect (false); + } + } + + [Test] + public void TestNotSupportedExceptions () + { + var commands = new List { + new ImapReplayCommand ("", "dovecot.greeting.txt"), + new ImapReplayCommand ("A00000000 LOGIN username password\r\n", "dovecot.authenticate+gmail-capabilities.txt"), + new ImapReplayCommand ("A00000001 NAMESPACE\r\n", "dovecot.namespace.txt"), + new ImapReplayCommand ("A00000002 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-inbox.txt"), + new ImapReplayCommand ("A00000003 LIST (SPECIAL-USE) \"\" \"*\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-special-use.txt"), + new ImapReplayCommand ("A00000004 SELECT INBOX\r\n", "common.select-inbox-no-modseq.txt") + }; + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + var credentials = new NetworkCredential ("username", "password"); + + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + client.Authenticate (credentials); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Inbox.SyncRoot, Is.InstanceOf (), "SyncRoot"); + + // disable all features + client.Capabilities = ImapCapabilities.None; + + var inbox = (ImapFolder) client.Inbox; + inbox.Open (FolderAccess.ReadWrite); + + // Fetch + var headers = new HashSet (new HeaderId[] { HeaderId.Subject }); + var fields = new HashSet (new string[] { "SUBJECT" }); + var uids = new UniqueId[] { UniqueId.MinValue }; + var indexes = new int[] { 0 }; + ulong modseq = 409601020304; + + Assert.Throws (() => inbox.Fetch (0, -1, modseq, MessageSummaryItems.All)); + Assert.ThrowsAsync (async () => await inbox.FetchAsync (0, -1, modseq, MessageSummaryItems.All)); + Assert.Throws (() => inbox.Fetch (0, -1, modseq, MessageSummaryItems.All, headers)); + Assert.ThrowsAsync (async () => await inbox.FetchAsync (0, -1, modseq, MessageSummaryItems.All, headers)); + Assert.Throws (() => inbox.Fetch (0, -1, modseq, MessageSummaryItems.All, fields)); + Assert.ThrowsAsync (async () => await inbox.FetchAsync (0, -1, modseq, MessageSummaryItems.All, fields)); + + Assert.Throws (() => inbox.Fetch (indexes, modseq, MessageSummaryItems.All)); + Assert.ThrowsAsync (async () => await inbox.FetchAsync (indexes, modseq, MessageSummaryItems.All)); + Assert.Throws (() => inbox.Fetch (indexes, modseq, MessageSummaryItems.All, headers)); + Assert.ThrowsAsync (async () => await inbox.FetchAsync (indexes, modseq, MessageSummaryItems.All, headers)); + Assert.Throws (() => inbox.Fetch (indexes, modseq, MessageSummaryItems.All, fields)); + Assert.ThrowsAsync (async () => await inbox.FetchAsync (indexes, modseq, MessageSummaryItems.All, fields)); + + Assert.Throws (() => inbox.Fetch (uids, modseq, MessageSummaryItems.All)); + Assert.ThrowsAsync (async () => await inbox.FetchAsync (uids, modseq, MessageSummaryItems.All)); + Assert.Throws (() => inbox.Fetch (uids, modseq, MessageSummaryItems.All, headers)); + Assert.ThrowsAsync (async () => await inbox.FetchAsync (uids, modseq, MessageSummaryItems.All, headers)); + Assert.Throws (() => inbox.Fetch (uids, modseq, MessageSummaryItems.All, fields)); + Assert.ThrowsAsync (async () => await inbox.FetchAsync (uids, modseq, MessageSummaryItems.All, fields)); + + client.Disconnect (false); + } + } + + static List CreateEmptyFetchRequestCommands () + { + return new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt"), + new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "gmail.authenticate.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-inbox.txt"), + new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt"), + new ImapReplayCommand ("A00000005 LIST \"\" \"%\"\r\n", "gmail.list-personal.txt"), + new ImapReplayCommand ("A00000006 EXAMINE INBOX (CONDSTORE)\r\n", "gmail.examine-inbox.txt"), + }; + } + + [Test] + public void TestEmptyFetchRequest () + { + var commands = CreateEmptyFetchRequestCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: Do not try XOAUTH2 + client.AuthenticationMechanisms.Remove ("XOAUTH2"); + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + // disable LIST-EXTENDED + client.Capabilities &= ~ImapCapabilities.ListExtended; + + var personal = client.GetFolder (client.PersonalNamespaces[0]); + var folders = personal.GetSubfolders (); + Assert.That (folders[0], Is.EqualTo (client.Inbox), "Expected the first folder to be the Inbox."); + Assert.That (folders[1].FullName, Is.EqualTo ("[Gmail]"), "Expected the second folder to be [Gmail]."); + Assert.That (folders[1].Attributes, Is.EqualTo (FolderAttributes.NoSelect | FolderAttributes.HasChildren), "Expected [Gmail] folder to be \\Noselect \\HasChildren."); + + var inbox = client.Inbox; + + inbox.Open (FolderAccess.ReadOnly); + + // First, test a non-empty requests with empty message sets + var request = new FetchRequest (MessageSummaryItems.Flags); + + var messages = inbox.Fetch (Array.Empty (), request); + Assert.That (messages, Is.Empty, "UID FETCH (0 uids)"); + + messages = inbox.Fetch (Array.Empty (), request); + Assert.That (messages, Is.Empty, "FETCH (0 indexes)"); + + // Now make the FetchRequest empty + request = new FetchRequest (MessageSummaryItems.None); + + messages = inbox.Fetch (UniqueIdRange.All, request); + Assert.That (messages, Is.Empty, "UID FETCH (None)"); + + messages = inbox.Fetch (new int[] { 0, 1, 2, 3, 4, 5 }, request); + Assert.That (messages, Is.Empty, "FETCH (None)"); + + messages = inbox.Fetch (0, -1, request); + Assert.That (messages, Is.Empty, "FETCH min:max (None)"); + + client.Disconnect (false); + } + } + + [Test] + public async Task TestEmptyFetchRequestAsync () + { + var commands = CreateEmptyFetchRequestCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: Do not try XOAUTH2 + client.AuthenticationMechanisms.Remove ("XOAUTH2"); + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + // disable LIST-EXTENDED + client.Capabilities &= ~ImapCapabilities.ListExtended; + + var personal = client.GetFolder (client.PersonalNamespaces[0]); + var folders = await personal.GetSubfoldersAsync (); + Assert.That (folders[0], Is.EqualTo (client.Inbox), "Expected the first folder to be the Inbox."); + Assert.That (folders[1].FullName, Is.EqualTo ("[Gmail]"), "Expected the second folder to be [Gmail]."); + Assert.That (folders[1].Attributes, Is.EqualTo (FolderAttributes.NoSelect | FolderAttributes.HasChildren), "Expected [Gmail] folder to be \\Noselect \\HasChildren."); + + var inbox = client.Inbox; + + await inbox.OpenAsync (FolderAccess.ReadOnly); + + // First, test a non-empty requests with empty message sets + var request = new FetchRequest (MessageSummaryItems.Flags); + + var messages = await inbox.FetchAsync (Array.Empty (), request); + Assert.That (messages, Is.Empty, "UID FETCH (0 uids)"); + + messages = await inbox.FetchAsync (Array.Empty (), request); + Assert.That (messages, Is.Empty, "FETCH (0 indexes)"); + + // Now make the FetchRequest empty + request = new FetchRequest (MessageSummaryItems.None); + + messages = await inbox.FetchAsync (UniqueIdRange.All, request); + Assert.That (messages, Is.Empty, "UID FETCH (None)"); + + messages = await inbox.FetchAsync (new int[] { 0, 1, 2, 3, 4, 5 }, request); + Assert.That (messages, Is.Empty, "FETCH (None)"); + + messages = await inbox.FetchAsync (0, -1, request); + Assert.That (messages, Is.Empty, "FETCH min:max (None)"); + + await client.DisconnectAsync (false); + } + } + + static List CreateFetchAllHeadersCommands () + { + return new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt"), + new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "gmail.authenticate.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-inbox.txt"), + new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt"), + new ImapReplayCommand ("A00000005 LIST \"\" \"%\"\r\n", "gmail.list-personal.txt"), + new ImapReplayCommand ("A00000006 EXAMINE INBOX (CONDSTORE)\r\n", "gmail.examine-inbox.txt"), + new ImapReplayCommand ("A00000007 UID FETCH 1:* (UID FLAGS BODY.PEEK[HEADER])\r\n", "gmail.fetch-all-headers.txt"), + new ImapReplayCommand ("A00000008 FETCH 1:6 (UID FLAGS BODY.PEEK[HEADER])\r\n", "gmail.fetch-all-headers.txt"), + new ImapReplayCommand ("A00000009 FETCH 1:* (UID FLAGS BODY.PEEK[HEADER])\r\n", "gmail.fetch-all-headers.txt") + }; + } + + [Test] + public void TestFetchAllHeaders () + { + var commands = CreateFetchAllHeadersCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: Do not try XOAUTH2 + client.AuthenticationMechanisms.Remove ("XOAUTH2"); + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + // disable LIST-EXTENDED + client.Capabilities &= ~ImapCapabilities.ListExtended; + + var personal = client.GetFolder (client.PersonalNamespaces[0]); + var folders = personal.GetSubfolders (); + Assert.That (folders[0], Is.EqualTo (client.Inbox), "Expected the first folder to be the Inbox."); + Assert.That (folders[1].FullName, Is.EqualTo ("[Gmail]"), "Expected the second folder to be [Gmail]."); + Assert.That (folders[1].Attributes, Is.EqualTo (FolderAttributes.NoSelect | FolderAttributes.HasChildren), "Expected [Gmail] folder to be \\Noselect \\HasChildren."); + + var inbox = client.Inbox; + + inbox.Open (FolderAccess.ReadOnly); + + var request = new FetchRequest (MessageSummaryItems.Flags | MessageSummaryItems.UniqueId) { + Headers = HeaderSet.All + }; + + var messages = inbox.Fetch (UniqueIdRange.All, request); + Assert.That (messages, Has.Count.EqualTo (6), "UID FETCH"); + for (int i = 0; i < messages.Count; i++) + Assert.That (messages[i].Fields, Is.EqualTo (request.Items | MessageSummaryItems.Headers | MessageSummaryItems.References), "UID FETCH fields"); + + messages = inbox.Fetch (new int[] { 0, 1, 2, 3, 4, 5 }, request); + Assert.That (messages, Has.Count.EqualTo (6), "FETCH"); + for (int i = 0; i < messages.Count; i++) + Assert.That (messages[i].Fields, Is.EqualTo (request.Items | MessageSummaryItems.Headers | MessageSummaryItems.References), "FETCH fields"); + + messages = inbox.Fetch (0, -1, request); + Assert.That (messages, Has.Count.EqualTo (6), "FETCH min:max"); + for (int i = 0; i < messages.Count; i++) + Assert.That (messages[i].Fields, Is.EqualTo (request.Items | MessageSummaryItems.Headers | MessageSummaryItems.References), "FETCH min:max fields"); + + client.Disconnect (false); + } + } + + [Test] + public async Task TestFetchAllHeadersAsync () + { + var commands = CreateFetchAllHeadersCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: Do not try XOAUTH2 + client.AuthenticationMechanisms.Remove ("XOAUTH2"); + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + // disable LIST-EXTENDED + client.Capabilities &= ~ImapCapabilities.ListExtended; + + var personal = client.GetFolder (client.PersonalNamespaces[0]); + var folders = await personal.GetSubfoldersAsync (); + Assert.That (folders[0], Is.EqualTo (client.Inbox), "Expected the first folder to be the Inbox."); + Assert.That (folders[1].FullName, Is.EqualTo ("[Gmail]"), "Expected the second folder to be [Gmail]."); + Assert.That (folders[1].Attributes, Is.EqualTo (FolderAttributes.NoSelect | FolderAttributes.HasChildren), "Expected [Gmail] folder to be \\Noselect \\HasChildren."); + + var inbox = client.Inbox; + + await inbox.OpenAsync (FolderAccess.ReadOnly); + + var request = new FetchRequest (MessageSummaryItems.Flags | MessageSummaryItems.UniqueId) { + Headers = HeaderSet.All + }; + + var messages = await inbox.FetchAsync (UniqueIdRange.All, request); + Assert.That (messages, Has.Count.EqualTo (6), "UID FETCH"); + for (int i = 0; i < messages.Count; i++) + Assert.That (messages[i].Fields, Is.EqualTo (request.Items | MessageSummaryItems.Headers | MessageSummaryItems.References), "UID FETCH fields"); + + messages = await inbox.FetchAsync (new int[] { 0, 1, 2, 3, 4, 5 }, request); + Assert.That (messages, Has.Count.EqualTo (6), "FETCH"); + for (int i = 0; i < messages.Count; i++) + Assert.That (messages[i].Fields, Is.EqualTo (request.Items | MessageSummaryItems.Headers | MessageSummaryItems.References), "FETCH fields"); + + messages = await inbox.FetchAsync (0, -1, request); + Assert.That (messages, Has.Count.EqualTo (6), "FETCH min:max"); + for (int i = 0; i < messages.Count; i++) + Assert.That (messages[i].Fields, Is.EqualTo (request.Items | MessageSummaryItems.Headers | MessageSummaryItems.References), "FETCH min:max fields"); + + await client.DisconnectAsync (false); + } + } + + static List CreateFetchInvalidHeadersCommands () + { + return new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt"), + new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "gmail.authenticate.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-inbox.txt"), + new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt"), + new ImapReplayCommand ("A00000005 LIST \"\" \"%\"\r\n", "gmail.list-personal.txt"), + new ImapReplayCommand ("A00000006 EXAMINE INBOX (CONDSTORE)\r\n", "gmail.examine-inbox.txt"), + new ImapReplayCommand ("A00000007 UID FETCH 1:* (UID FLAGS BODY.PEEK[HEADER])\r\n", "gmail.fetch-invalid-headers.txt"), + new ImapReplayCommand ("A00000008 FETCH 1:6 (UID FLAGS BODY.PEEK[HEADER])\r\n", "gmail.fetch-invalid-headers.txt"), + new ImapReplayCommand ("A00000009 FETCH 1:* (UID FLAGS BODY.PEEK[HEADER])\r\n", "gmail.fetch-invalid-headers.txt") + }; + } + + [Test] + public void TestFetchInvalidHeaders () + { + var commands = CreateFetchInvalidHeadersCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: Do not try XOAUTH2 + client.AuthenticationMechanisms.Remove ("XOAUTH2"); + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + // disable LIST-EXTENDED + client.Capabilities &= ~ImapCapabilities.ListExtended; + + var personal = client.GetFolder (client.PersonalNamespaces[0]); + var folders = personal.GetSubfolders (); + Assert.That (folders[0], Is.EqualTo (client.Inbox), "Expected the first folder to be the Inbox."); + Assert.That (folders[1].FullName, Is.EqualTo ("[Gmail]"), "Expected the second folder to be [Gmail]."); + Assert.That (folders[1].Attributes, Is.EqualTo (FolderAttributes.NoSelect | FolderAttributes.HasChildren), "Expected [Gmail] folder to be \\Noselect \\HasChildren."); + + var inbox = client.Inbox; + + inbox.Open (FolderAccess.ReadOnly); + + var request = new FetchRequest (MessageSummaryItems.Flags | MessageSummaryItems.UniqueId) { + Headers = HeaderSet.All + }; + + var messages = inbox.Fetch (UniqueIdRange.All, request); + Assert.That (messages, Has.Count.EqualTo (6), "UID FETCH"); + for (int i = 0; i < messages.Count; i++) + Assert.That (messages[i].Fields, Is.EqualTo (request.Items | MessageSummaryItems.Headers | MessageSummaryItems.References), "UID FETCH fields"); + + messages = inbox.Fetch (new int[] { 0, 1, 2, 3, 4, 5 }, request); + Assert.That (messages, Has.Count.EqualTo (6), "FETCH"); + for (int i = 0; i < messages.Count; i++) + Assert.That (messages[i].Fields, Is.EqualTo (request.Items | MessageSummaryItems.Headers | MessageSummaryItems.References), "FETCH fields"); + + messages = inbox.Fetch (0, -1, request); + Assert.That (messages, Has.Count.EqualTo (6), "FETCH min:max"); + for (int i = 0; i < messages.Count; i++) + Assert.That (messages[i].Fields, Is.EqualTo (request.Items | MessageSummaryItems.Headers | MessageSummaryItems.References), "FETCH min:max fields"); + + client.Disconnect (false); + } + } + + [Test] + public async Task TestFetchInvalidHeadersAsync () + { + var commands = CreateFetchInvalidHeadersCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: Do not try XOAUTH2 + client.AuthenticationMechanisms.Remove ("XOAUTH2"); + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + // disable LIST-EXTENDED + client.Capabilities &= ~ImapCapabilities.ListExtended; + + var personal = client.GetFolder (client.PersonalNamespaces[0]); + var folders = await personal.GetSubfoldersAsync (); + Assert.That (folders[0], Is.EqualTo (client.Inbox), "Expected the first folder to be the Inbox."); + Assert.That (folders[1].FullName, Is.EqualTo ("[Gmail]"), "Expected the second folder to be [Gmail]."); + Assert.That (folders[1].Attributes, Is.EqualTo (FolderAttributes.NoSelect | FolderAttributes.HasChildren), "Expected [Gmail] folder to be \\Noselect \\HasChildren."); + + var inbox = client.Inbox; + + await inbox.OpenAsync (FolderAccess.ReadOnly); + + var request = new FetchRequest (MessageSummaryItems.Flags | MessageSummaryItems.UniqueId) { + Headers = HeaderSet.All + }; + + var messages = await inbox.FetchAsync (UniqueIdRange.All, request); + Assert.That (messages, Has.Count.EqualTo (6), "UID FETCH"); + for (int i = 0; i < messages.Count; i++) + Assert.That (messages[i].Fields, Is.EqualTo (request.Items | MessageSummaryItems.Headers | MessageSummaryItems.References), "UID FETCH fields"); + + messages = await inbox.FetchAsync (new int[] { 0, 1, 2, 3, 4, 5 }, request); + Assert.That (messages, Has.Count.EqualTo (6), "FETCH"); + for (int i = 0; i < messages.Count; i++) + Assert.That (messages[i].Fields, Is.EqualTo (request.Items | MessageSummaryItems.Headers | MessageSummaryItems.References), "FETCH fields"); + + messages = await inbox.FetchAsync (0, -1, request); + Assert.That (messages, Has.Count.EqualTo (6), "FETCH min:max"); + for (int i = 0; i < messages.Count; i++) + Assert.That (messages[i].Fields, Is.EqualTo (request.Items | MessageSummaryItems.Headers | MessageSummaryItems.References), "FETCH min:max fields"); + + await client.DisconnectAsync (false); + } + } + + static readonly string[] PreviewTextValues = { + "Planet Fitness https://view.email.planetfitness.com/?qs=9a098a031cabde68c0a4260051cd6fe473a2e997a53678ff26b4b199a711a9d2ad0536530d6f837c246b09f644d42016ecfb298f930b7af058e9e454b34f3d818ceb3052ae317b1ac4594aab28a2d788 View web ver", + "Don't miss our celebrity guest Monday evening", + "Planet Fitness https://view.email.planetfitness.com/?qs=9a098a031cabde68c0a4260051cd6fe473a2e997a53678ff26b4b199a711a9d2ad0536530d6f837c246b09f644d42016ecfb298f930b7af058e9e454b34f3d818ceb3052ae317b1ac4594aab28a2d788 View web ver", + "Planet Fitness https://view.email.planetfitness.com/?qs=9a098a031cabde68c0a4260051cd6fe473a2e997a53678ff26b4b199a711a9d2ad0536530d6f837c246b09f644d42016ecfb298f930b7af058e9e454b34f3d818ceb3052ae317b1ac4594aab28a2d788 View web ver", + "Don't miss our celebrity guest Monday evening", + "Planet Fitness https://view.email.planetfitness.com/?qs=9a098a031cabde68c0a4260051cd6fe473a2e997a53678ff26b4b199a711a9d2ad0536530d6f837c246b09f644d42016ecfb298f930b7af058e9e454b34f3d818ceb3052ae317b1ac4594aab28a2d788 View web ver" + }; + + static List CreateFetchPreviewTextCommands () + { + return new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt"), + new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "gmail.authenticate+preview.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-inbox.txt"), + new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt"), + new ImapReplayCommand ("A00000005 LIST \"\" \"%\"\r\n", "gmail.list-personal.txt"), + new ImapReplayCommand ("A00000006 EXAMINE INBOX (CONDSTORE)\r\n", "gmail.examine-inbox.txt"), + new ImapReplayCommand ("A00000007 UID FETCH 1:* (FLAGS INTERNALDATE RFC822.SIZE ENVELOPE PREVIEW)\r\n", "gmail.fetch-preview.txt"), + new ImapReplayCommand ("A00000008 FETCH 1:6 (FLAGS INTERNALDATE RFC822.SIZE ENVELOPE PREVIEW)\r\n", "gmail.fetch-preview.txt"), + new ImapReplayCommand ("A00000009 FETCH 1:* (FLAGS INTERNALDATE RFC822.SIZE ENVELOPE PREVIEW)\r\n", "gmail.fetch-preview.txt") + }; + } + + [Test] + public void TestFetchPreviewText () + { + var commands = CreateFetchPreviewTextCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: Do not try XOAUTH2 + client.AuthenticationMechanisms.Remove ("XOAUTH2"); + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + // disable LIST-EXTENDED + client.Capabilities &= ~ImapCapabilities.ListExtended; + + var personal = client.GetFolder (client.PersonalNamespaces[0]); + var folders = personal.GetSubfolders (); + Assert.That (folders[0], Is.EqualTo (client.Inbox), "Expected the first folder to be the Inbox."); + Assert.That (folders[1].FullName, Is.EqualTo ("[Gmail]"), "Expected the second folder to be [Gmail]."); + Assert.That (folders[1].Attributes, Is.EqualTo (FolderAttributes.NoSelect | FolderAttributes.HasChildren), "Expected [Gmail] folder to be \\Noselect \\HasChildren."); + + var inbox = client.Inbox; + + inbox.Open (FolderAccess.ReadOnly); + + var messages = inbox.Fetch (UniqueIdRange.All, MessageSummaryItems.All | MessageSummaryItems.PreviewText); + Assert.That (messages, Has.Count.EqualTo (PreviewTextValues.Length), "UID FETCH"); + for (int i = 0; i < messages.Count; i++) + Assert.That (messages[i].PreviewText, Is.EqualTo (PreviewTextValues[i])); + + messages = inbox.Fetch (new int[] { 0, 1, 2, 3, 4, 5 }, MessageSummaryItems.All | MessageSummaryItems.PreviewText); + Assert.That (messages, Has.Count.EqualTo (PreviewTextValues.Length), "FETCH"); + for (int i = 0; i < messages.Count; i++) + Assert.That (messages[i].PreviewText, Is.EqualTo (PreviewTextValues[i])); + + messages = inbox.Fetch (0, -1, MessageSummaryItems.All | MessageSummaryItems.PreviewText); + Assert.That (messages, Has.Count.EqualTo (PreviewTextValues.Length), "FETCH min:max"); + for (int i = 0; i < messages.Count; i++) + Assert.That (messages[i].PreviewText, Is.EqualTo (PreviewTextValues[i])); + + client.Disconnect (false); + } + } + + [Test] + public async Task TestFetchPreviewTextAsync () + { + var commands = CreateFetchPreviewTextCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: Do not try XOAUTH2 + client.AuthenticationMechanisms.Remove ("XOAUTH2"); + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + // disable LIST-EXTENDED + client.Capabilities &= ~ImapCapabilities.ListExtended; + + var personal = client.GetFolder (client.PersonalNamespaces[0]); + var folders = await personal.GetSubfoldersAsync (); + Assert.That (folders[0], Is.EqualTo (client.Inbox), "Expected the first folder to be the Inbox."); + Assert.That (folders[1].FullName, Is.EqualTo ("[Gmail]"), "Expected the second folder to be [Gmail]."); + Assert.That (folders[1].Attributes, Is.EqualTo (FolderAttributes.NoSelect | FolderAttributes.HasChildren), "Expected [Gmail] folder to be \\Noselect \\HasChildren."); + + var inbox = client.Inbox; + + await inbox.OpenAsync (FolderAccess.ReadOnly); + + var messages = await inbox.FetchAsync (UniqueIdRange.All, MessageSummaryItems.All | MessageSummaryItems.PreviewText); + Assert.That (messages, Has.Count.EqualTo (PreviewTextValues.Length), "UID FETCH"); + for (int i = 0; i < messages.Count; i++) + Assert.That (messages[i].PreviewText, Is.EqualTo (PreviewTextValues[i])); + + messages = await inbox.FetchAsync (new int[] { 0, 1, 2, 3, 4, 5 }, MessageSummaryItems.All | MessageSummaryItems.PreviewText); + Assert.That (messages, Has.Count.EqualTo (PreviewTextValues.Length), "FETCH"); + for (int i = 0; i < messages.Count; i++) + Assert.That (messages[i].PreviewText, Is.EqualTo (PreviewTextValues[i])); + + messages = await inbox.FetchAsync (0, -1, MessageSummaryItems.All | MessageSummaryItems.PreviewText); + Assert.That (messages, Has.Count.EqualTo (PreviewTextValues.Length), "FETCH min:max"); + for (int i = 0; i < messages.Count; i++) + Assert.That (messages[i].PreviewText, Is.EqualTo (PreviewTextValues[i])); + + await client.DisconnectAsync (false); + } + } + +#if ENABLE_LAZY_PREVIEW_API + static List CreateFetchLazyPreviewTextCommands () + { + return new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt"), + new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "gmail.authenticate+preview.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-inbox.txt"), + new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt"), + new ImapReplayCommand ("A00000005 LIST \"\" \"%\"\r\n", "gmail.list-personal.txt"), + new ImapReplayCommand ("A00000006 EXAMINE INBOX (CONDSTORE)\r\n", "gmail.examine-inbox.txt"), + new ImapReplayCommand ("A00000007 UID FETCH 1:* (FLAGS INTERNALDATE RFC822.SIZE ENVELOPE PREVIEW (LAZY))\r\n", "gmail.fetch-preview.txt"), + new ImapReplayCommand ("A00000008 FETCH 1:6 (FLAGS INTERNALDATE RFC822.SIZE ENVELOPE PREVIEW (LAZY))\r\n", "gmail.fetch-preview.txt"), + new ImapReplayCommand ("A00000009 FETCH 1:* (FLAGS INTERNALDATE RFC822.SIZE ENVELOPE PREVIEW (LAZY))\r\n", "gmail.fetch-preview.txt") + }; + } + + [Test] + public void TestFetchLazyPreviewText () + { + var commands = CreateFetchLazyPreviewTextCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: Do not try XOAUTH2 + client.AuthenticationMechanisms.Remove ("XOAUTH2"); + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + // disable LIST-EXTENDED + client.Capabilities &= ~ImapCapabilities.ListExtended; + + var personal = client.GetFolder (client.PersonalNamespaces[0]); + var folders = personal.GetSubfolders (); + Assert.That (folders[0], Is.EqualTo (client.Inbox), "Expected the first folder to be the Inbox."); + Assert.That (folders[1].FullName, Is.EqualTo ("[Gmail]"), "Expected the second folder to be [Gmail]."); + Assert.That (folders[1].Attributes, Is.EqualTo (FolderAttributes.NoSelect | FolderAttributes.HasChildren), "Expected [Gmail] folder to be \\Noselect \\HasChildren."); + + var inbox = client.Inbox; + + inbox.Open (FolderAccess.ReadOnly); + + var request = new FetchRequest (MessageSummaryItems.All | MessageSummaryItems.PreviewText) { + PreviewOptions = PreviewOptions.Lazy + }; + + var messages = inbox.Fetch (UniqueIdRange.All, request); + for (int i = 0; i < messages.Count; i++) + Assert.That (messages[i].PreviewText, Is.EqualTo (PreviewTextValues[i])); + + messages = inbox.Fetch (new int[] { 0, 1, 2, 3, 4, 5 }, request); + for (int i = 0; i < messages.Count; i++) + Assert.That (messages[i].PreviewText, Is.EqualTo (PreviewTextValues[i])); + + messages = inbox.Fetch (0, -1, request); + for (int i = 0; i < messages.Count; i++) + Assert.That (messages[i].PreviewText, Is.EqualTo (PreviewTextValues[i])); + + client.Disconnect (false); + } + } + + [Test] + public async Task TestFetchLazyPreviewTextAsync () + { + var commands = CreateFetchLazyPreviewTextCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: Do not try XOAUTH2 + client.AuthenticationMechanisms.Remove ("XOAUTH2"); + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + // disable LIST-EXTENDED + client.Capabilities &= ~ImapCapabilities.ListExtended; + + var personal = client.GetFolder (client.PersonalNamespaces[0]); + var folders = await personal.GetSubfoldersAsync (); + Assert.That (folders[0], Is.EqualTo (client.Inbox), "Expected the first folder to be the Inbox."); + Assert.That (folders[1].FullName, Is.EqualTo ("[Gmail]"), "Expected the second folder to be [Gmail]."); + Assert.That (folders[1].Attributes, Is.EqualTo (FolderAttributes.NoSelect | FolderAttributes.HasChildren), "Expected [Gmail] folder to be \\Noselect \\HasChildren."); + + var inbox = client.Inbox; + + await inbox.OpenAsync (FolderAccess.ReadOnly); + + var request = new FetchRequest (MessageSummaryItems.All | MessageSummaryItems.PreviewText) { + PreviewOptions = PreviewOptions.Lazy + }; + + var messages = await inbox.FetchAsync (UniqueIdRange.All, request); + for (int i = 0; i < messages.Count; i++) + Assert.That (messages[i].PreviewText, Is.EqualTo (PreviewTextValues[i])); + + messages = await inbox.FetchAsync (new int[] { 0, 1, 2, 3, 4, 5 }, request); + for (int i = 0; i < messages.Count; i++) + Assert.That (messages[i].PreviewText, Is.EqualTo (PreviewTextValues[i])); + + messages = await inbox.FetchAsync (0, -1, request); + for (int i = 0; i < messages.Count; i++) + Assert.That (messages[i].PreviewText, Is.EqualTo (PreviewTextValues[i])); + + await client.DisconnectAsync (false); + } + } +#endif + + static readonly string[] SimulatedPreviewTextValues = { + "Planet Fitness https://view.email.planetfitness.com/?qs=9a098a031cabde68c0a4260051cd6fe473a2e997a53678ff26b4b199a711a9d2ad0536530d6f837c246b09f644d42016ecfb298f930b7af058e9e454b34f3d818ceb3052ae317b1ac4594aab28a2d788 View web ver…", + "Don’t miss our celebrity guest Monday evening", + "Planet Fitness https://view.email.planetfitness.com/?qs=9a098a031cabde68c0a4260051cd6fe473a2e997a53678ff26b4b199a711a9d2ad0536530d6f837c246b09f644d42016ecfb298f930b7af058e9e454b34f3d818ceb3052ae317b1ac4594aab28a2d788 View web ver…", + "Planet Fitness https://view.email.planetfitness.com/?qs=9a098a031cabde68c0a4260051cd6fe473a2e997a53678ff26b4b199a711a9d2ad0536530d6f837c246b09f644d42016ecfb298f930b7af058e9e454b34f3d818ceb3052ae317b1ac4594aab28a2d788 View web ver…", + "Don’t miss our celebrity guest Monday evening", + "Planet Fitness https://view.email.planetfitness.com/?qs=9a098a031cabde68c0a4260051cd6fe473a2e997a53678ff26b4b199a711a9d2ad0536530d6f837c246b09f644d42016ecfb298f930b7af058e9e454b34f3d818ceb3052ae317b1ac4594aab28a2d788 View web ver…", + string.Empty + }; + + static List CreateFetchSimulatedPreviewTextCommands () + { + return new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt"), + new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "gmail.authenticate.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-inbox.txt"), + new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt"), + new ImapReplayCommand ("A00000005 LIST \"\" \"%\"\r\n", "gmail.list-personal.txt"), + new ImapReplayCommand ("A00000006 EXAMINE INBOX (CONDSTORE)\r\n", "gmail.examine-inbox.txt"), + new ImapReplayCommand ("A00000007 UID FETCH 1:* (UID FLAGS INTERNALDATE RFC822.SIZE ENVELOPE BODYSTRUCTURE)\r\n", "gmail.fetch-previewtext-bodystructure.txt"), + new ImapReplayCommand ("A00000008 UID FETCH 1,4 (BODY.PEEK[TEXT]<0.512>)\r\n", "gmail.fetch-previewtext-peek-text-only.txt"), + new ImapReplayCommand ("A00000009 UID FETCH 3,6 (BODY.PEEK[1]<0.512>)\r\n", "gmail.fetch-previewtext-peek-text-alternative.txt"), + new ImapReplayCommand ("A00000010 UID FETCH 2,5 (BODY.PEEK[TEXT]<0.16384>)\r\n", "gmail.fetch-previewtext-peek-html-only.txt"), + new ImapReplayCommand ("A00000011 FETCH 1:7 (UID FLAGS INTERNALDATE RFC822.SIZE ENVELOPE BODYSTRUCTURE)\r\n", "gmail.fetch-previewtext-bodystructure.txt"), + new ImapReplayCommand ("A00000012 UID FETCH 1,4 (BODY.PEEK[TEXT]<0.512>)\r\n", "gmail.fetch-previewtext-peek-text-only.txt"), + new ImapReplayCommand ("A00000013 UID FETCH 3,6 (BODY.PEEK[1]<0.512>)\r\n", "gmail.fetch-previewtext-peek-text-alternative.txt"), + new ImapReplayCommand ("A00000014 UID FETCH 2,5 (BODY.PEEK[TEXT]<0.16384>)\r\n", "gmail.fetch-previewtext-peek-html-only.txt"), + new ImapReplayCommand ("A00000015 FETCH 1:* (UID FLAGS INTERNALDATE RFC822.SIZE ENVELOPE BODYSTRUCTURE)\r\n", "gmail.fetch-previewtext-bodystructure.txt"), + new ImapReplayCommand ("A00000016 UID FETCH 1,4 (BODY.PEEK[TEXT]<0.512>)\r\n", "gmail.fetch-previewtext-peek-text-only.txt"), + new ImapReplayCommand ("A00000017 UID FETCH 3,6 (BODY.PEEK[1]<0.512>)\r\n", "gmail.fetch-previewtext-peek-text-alternative.txt"), + new ImapReplayCommand ("A00000018 UID FETCH 2,5 (BODY.PEEK[TEXT]<0.16384>)\r\n", "gmail.fetch-previewtext-peek-html-only.txt") + }; + } + + [Test] + public void TestFetchSimulatedPreviewText () + { + var commands = CreateFetchSimulatedPreviewTextCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: Do not try XOAUTH2 + client.AuthenticationMechanisms.Remove ("XOAUTH2"); + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + // disable LIST-EXTENDED + client.Capabilities &= ~ImapCapabilities.ListExtended; + + var personal = client.GetFolder (client.PersonalNamespaces[0]); + var folders = personal.GetSubfolders (); + Assert.That (folders[0], Is.EqualTo (client.Inbox), "Expected the first folder to be the Inbox."); + Assert.That (folders[1].FullName, Is.EqualTo ("[Gmail]"), "Expected the second folder to be [Gmail]."); + Assert.That (folders[1].Attributes, Is.EqualTo (FolderAttributes.NoSelect | FolderAttributes.HasChildren), "Expected [Gmail] folder to be \\Noselect \\HasChildren."); + + var inbox = client.Inbox; + + inbox.Open (FolderAccess.ReadOnly); + + var messages = inbox.Fetch (UniqueIdRange.All, MessageSummaryItems.Full | MessageSummaryItems.PreviewText); + for (int i = 0; i < messages.Count; i++) + Assert.That (messages[i].PreviewText, Is.EqualTo (SimulatedPreviewTextValues[i])); + + messages = inbox.Fetch (new int[] { 0, 1, 2, 3, 4, 5, 6 }, MessageSummaryItems.Full | MessageSummaryItems.PreviewText); + for (int i = 0; i < messages.Count; i++) + Assert.That (messages[i].PreviewText, Is.EqualTo (SimulatedPreviewTextValues[i])); + + messages = inbox.Fetch (0, -1, MessageSummaryItems.Full | MessageSummaryItems.PreviewText); + for (int i = 0; i < messages.Count; i++) + Assert.That (messages[i].PreviewText, Is.EqualTo (SimulatedPreviewTextValues[i])); + + client.Disconnect (false); + } + } + + [Test] + public async Task TestFetchSimulatedPreviewTextAsync () + { + var commands = CreateFetchSimulatedPreviewTextCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: Do not try XOAUTH2 + client.AuthenticationMechanisms.Remove ("XOAUTH2"); + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + // disable LIST-EXTENDED + client.Capabilities &= ~ImapCapabilities.ListExtended; + + var personal = client.GetFolder (client.PersonalNamespaces[0]); + var folders = await personal.GetSubfoldersAsync (); + Assert.That (folders[0], Is.EqualTo (client.Inbox), "Expected the first folder to be the Inbox."); + Assert.That (folders[1].FullName, Is.EqualTo ("[Gmail]"), "Expected the second folder to be [Gmail]."); + Assert.That (folders[1].Attributes, Is.EqualTo (FolderAttributes.NoSelect | FolderAttributes.HasChildren), "Expected [Gmail] folder to be \\Noselect \\HasChildren."); + + var inbox = client.Inbox; + + await inbox.OpenAsync (FolderAccess.ReadOnly); + + var messages = await inbox.FetchAsync (UniqueIdRange.All, MessageSummaryItems.Full | MessageSummaryItems.PreviewText); + for (int i = 0; i < messages.Count; i++) + Assert.That (messages[i].PreviewText, Is.EqualTo (SimulatedPreviewTextValues[i])); + + messages = await inbox.FetchAsync (new int[] { 0, 1, 2, 3, 4, 5, 6 }, MessageSummaryItems.Full | MessageSummaryItems.PreviewText); + for (int i = 0; i < messages.Count; i++) + Assert.That (messages[i].PreviewText, Is.EqualTo (SimulatedPreviewTextValues[i])); + + messages = await inbox.FetchAsync (0, -1, MessageSummaryItems.Full | MessageSummaryItems.PreviewText); + for (int i = 0; i < messages.Count; i++) + Assert.That (messages[i].PreviewText, Is.EqualTo (SimulatedPreviewTextValues[i])); + + await client.DisconnectAsync (false); + } + } + + static List CreateFetchSimulatedKoreanPreviewTextCommands () + { + return new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt"), + new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "gmail.authenticate.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-inbox.txt"), + new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt"), + new ImapReplayCommand ("A00000005 LIST \"\" \"%\"\r\n", "gmail.list-personal.txt"), + new ImapReplayCommand ("A00000006 EXAMINE INBOX (CONDSTORE)\r\n", "gmail.examine-inbox.txt"), + new ImapReplayCommand ("A00000007 UID FETCH 1 (UID BODYSTRUCTURE)\r\n", "gmail.fetch-korean-previewtext-bodystructure.txt"), + new ImapReplayCommand ("A00000008 UID FETCH 1 (BODY.PEEK[TEXT]<0.512>)\r\n", "gmail.fetch-korean-previewtext-peek-text-only.txt"), + }; + } + + [Test] + public void TestFetchSimulatedKoreanPreviewText () + { + const string koreanPreviewText = "서기 250년경 고분 시대가 시작되면서 고분이라고 불리는 거대한 무덤이 건설된 것은 보다 집약적인 농업과 철기 기술의 도입에 힘입어 강력한 전사 엘리트의 출현을 나타냅니다. 일본은 철과 기타 물품의 공급을 확보하기 위해 남한의 연안 지배 집단과 집중적인 접촉을 벌이면서 중국에 사신을 파견하면서 대륙 본토와의 접촉이 증가했습니다(238, 243, 247). 4세기 동안 지속된 한반도의 한국 세력과"; + var commands = CreateFetchSimulatedKoreanPreviewTextCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: Do not try XOAUTH2 + client.AuthenticationMechanisms.Remove ("XOAUTH2"); + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + // disable LIST-EXTENDED + client.Capabilities &= ~ImapCapabilities.ListExtended; + + var personal = client.GetFolder (client.PersonalNamespaces[0]); + var folders = personal.GetSubfolders (); + Assert.That (folders[0], Is.EqualTo (client.Inbox), "Expected the first folder to be the Inbox."); + Assert.That (folders[1].FullName, Is.EqualTo ("[Gmail]"), "Expected the second folder to be [Gmail]."); + Assert.That (folders[1].Attributes, Is.EqualTo (FolderAttributes.NoSelect | FolderAttributes.HasChildren), "Expected [Gmail] folder to be \\Noselect \\HasChildren."); + + var inbox = client.Inbox; + + inbox.Open (FolderAccess.ReadOnly); + + var messages = inbox.Fetch (new[] { new UniqueId (1) }, MessageSummaryItems.PreviewText); + Assert.That (messages.Count, Is.EqualTo (1), "Expected 1 message to be fetched."); + Assert.That (messages[0].PreviewText, Is.EqualTo (koreanPreviewText)); + + client.Disconnect (false); + } + } + + [Test] + public async Task TestFetchSimulatedKoreanPreviewTextAsync () + { + const string koreanPreviewText = "서기 250년경 고분 시대가 시작되면서 고분이라고 불리는 거대한 무덤이 건설된 것은 보다 집약적인 농업과 철기 기술의 도입에 힘입어 강력한 전사 엘리트의 출현을 나타냅니다. 일본은 철과 기타 물품의 공급을 확보하기 위해 남한의 연안 지배 집단과 집중적인 접촉을 벌이면서 중국에 사신을 파견하면서 대륙 본토와의 접촉이 증가했습니다(238, 243, 247). 4세기 동안 지속된 한반도의 한국 세력과"; + var commands = CreateFetchSimulatedKoreanPreviewTextCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: Do not try XOAUTH2 + client.AuthenticationMechanisms.Remove ("XOAUTH2"); + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + // disable LIST-EXTENDED + client.Capabilities &= ~ImapCapabilities.ListExtended; + + var personal = client.GetFolder (client.PersonalNamespaces[0]); + var folders = await personal.GetSubfoldersAsync (); + Assert.That (folders[0], Is.EqualTo (client.Inbox), "Expected the first folder to be the Inbox."); + Assert.That (folders[1].FullName, Is.EqualTo ("[Gmail]"), "Expected the second folder to be [Gmail]."); + Assert.That (folders[1].Attributes, Is.EqualTo (FolderAttributes.NoSelect | FolderAttributes.HasChildren), "Expected [Gmail] folder to be \\Noselect \\HasChildren."); + + var inbox = client.Inbox; + + await inbox.OpenAsync (FolderAccess.ReadOnly); + + var messages = await inbox.FetchAsync (new[] { new UniqueId (1) }, MessageSummaryItems.PreviewText); + Assert.That (messages.Count, Is.EqualTo (1), "Expected 1 message to be fetched."); + Assert.That (messages[0].PreviewText, Is.EqualTo (koreanPreviewText)); + + await client.DisconnectAsync (false); + } + } + + static List CreateFetchQuotedStringCommands () + { + return new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt"), + new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "gmail.authenticate.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-inbox.txt"), + new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt"), + new ImapReplayCommand ("A00000005 LIST \"\" \"%\"\r\n", "gmail.list-personal.txt"), + new ImapReplayCommand ("A00000006 EXAMINE INBOX (CONDSTORE)\r\n", "gmail.examine-inbox.txt"), + new ImapReplayCommand ("A00000007 FETCH 1:* (UID BODYSTRUCTURE)\r\n", "gmail.fetch-quoted-string-bodystructure.txt"), + new ImapReplayCommand ("A00000008 UID FETCH 1 (BODY.PEEK[1.TEXT]<0.512>)\r\n", "gmail.fetch-quoted-string.txt"), + }; + } + + [Test] + public void TestFetchQuotedString () + { + var commands = CreateFetchQuotedStringCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: Do not try XOAUTH2 + client.AuthenticationMechanisms.Remove ("XOAUTH2"); + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + // disable LIST-EXTENDED + client.Capabilities &= ~ImapCapabilities.ListExtended; + + var personal = client.GetFolder (client.PersonalNamespaces[0]); + var folders = personal.GetSubfolders (); + Assert.That (folders[0], Is.EqualTo (client.Inbox), "Expected the first folder to be the Inbox."); + Assert.That (folders[1].FullName, Is.EqualTo ("[Gmail]"), "Expected the second folder to be [Gmail]."); + Assert.That (folders[1].Attributes, Is.EqualTo (FolderAttributes.NoSelect | FolderAttributes.HasChildren), "Expected [Gmail] folder to be \\Noselect \\HasChildren."); + + var inbox = client.Inbox; + + inbox.Open (FolderAccess.ReadOnly); + + var messages = inbox.Fetch (0, -1, MessageSummaryItems.UniqueId | MessageSummaryItems.BodyStructure); + using (var stream = inbox.GetStream (messages[0].UniqueId, messages[0].TextBody.PartSpecifier + ".TEXT", 0, 512)) { + var text = Encoding.UTF8.GetString (((MemoryStream) stream).ToArray ()); + + Assert.That (text, Is.EqualTo ("This is the message body as a quoted-string."), "The message body does not match."); + } + + client.Disconnect (false); + } + } + + [Test] + public async Task TestFetchQuotedStringAsync () + { + var commands = CreateFetchQuotedStringCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: Do not try XOAUTH2 + client.AuthenticationMechanisms.Remove ("XOAUTH2"); + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + // disable LIST-EXTENDED + client.Capabilities &= ~ImapCapabilities.ListExtended; + + var personal = client.GetFolder (client.PersonalNamespaces[0]); + var folders = await personal.GetSubfoldersAsync (); + Assert.That (folders[0], Is.EqualTo (client.Inbox), "Expected the first folder to be the Inbox."); + Assert.That (folders[1].FullName, Is.EqualTo ("[Gmail]"), "Expected the second folder to be [Gmail]."); + Assert.That (folders[1].Attributes, Is.EqualTo (FolderAttributes.NoSelect | FolderAttributes.HasChildren), "Expected [Gmail] folder to be \\Noselect \\HasChildren."); + + var inbox = client.Inbox; + + await inbox.OpenAsync (FolderAccess.ReadOnly); + + var messages = await inbox.FetchAsync (0, -1, MessageSummaryItems.UniqueId | MessageSummaryItems.BodyStructure); + using (var stream = await inbox.GetStreamAsync (messages[0].UniqueId, messages[0].TextBody.PartSpecifier + ".TEXT", 0, 512)) { + var text = Encoding.UTF8.GetString (((MemoryStream) stream).ToArray ()); + + Assert.That (text, Is.EqualTo ("This is the message body as a quoted-string."), "The message body does not match."); + } + + await client.DisconnectAsync (false); + } + } + + static List CreateFetchNilCommands () + { + return new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt"), + new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "gmail.authenticate.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-inbox.txt"), + new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt"), + new ImapReplayCommand ("A00000005 LIST \"\" \"%\"\r\n", "gmail.list-personal.txt"), + new ImapReplayCommand ("A00000006 EXAMINE INBOX (CONDSTORE)\r\n", "gmail.examine-inbox.txt"), + new ImapReplayCommand ("A00000007 FETCH 1:* (UID BODYSTRUCTURE)\r\n", "gmail.fetch-nil-bodystructure.txt"), + new ImapReplayCommand ("A00000008 UID FETCH 1 (BODY.PEEK[1.TEXT]<0.512>)\r\n", "gmail.fetch-nil.txt"), + }; + } + + [Test] + public void TestFetchNil () + { + var commands = CreateFetchNilCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: Do not try XOAUTH2 + client.AuthenticationMechanisms.Remove ("XOAUTH2"); + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + // disable LIST-EXTENDED + client.Capabilities &= ~ImapCapabilities.ListExtended; + + var personal = client.GetFolder (client.PersonalNamespaces[0]); + var folders = personal.GetSubfolders (); + Assert.That (folders[0], Is.EqualTo (client.Inbox), "Expected the first folder to be the Inbox."); + Assert.That (folders[1].FullName, Is.EqualTo ("[Gmail]"), "Expected the second folder to be [Gmail]."); + Assert.That (folders[1].Attributes, Is.EqualTo (FolderAttributes.NoSelect | FolderAttributes.HasChildren), "Expected [Gmail] folder to be \\Noselect \\HasChildren."); + + var inbox = client.Inbox; + + inbox.Open (FolderAccess.ReadOnly); + + var messages = inbox.Fetch (0, -1, MessageSummaryItems.UniqueId | MessageSummaryItems.BodyStructure); + using (var stream = inbox.GetStream (messages[0].UniqueId, messages[0].TextBody.PartSpecifier + ".TEXT", 0, 512)) { + var text = Encoding.UTF8.GetString (((MemoryStream) stream).ToArray ()); + + Assert.That (text, Is.EqualTo (string.Empty), "The message body does not match."); + } + + client.Disconnect (false); + } + } + + [Test] + public async Task TestFetchNilAsync () + { + var commands = CreateFetchNilCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: Do not try XOAUTH2 + client.AuthenticationMechanisms.Remove ("XOAUTH2"); + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + // disable LIST-EXTENDED + client.Capabilities &= ~ImapCapabilities.ListExtended; + + var personal = client.GetFolder (client.PersonalNamespaces[0]); + var folders = await personal.GetSubfoldersAsync (); + Assert.That (folders[0], Is.EqualTo (client.Inbox), "Expected the first folder to be the Inbox."); + Assert.That (folders[1].FullName, Is.EqualTo ("[Gmail]"), "Expected the second folder to be [Gmail]."); + Assert.That (folders[1].Attributes, Is.EqualTo (FolderAttributes.NoSelect | FolderAttributes.HasChildren), "Expected [Gmail] folder to be \\Noselect \\HasChildren."); + + var inbox = client.Inbox; + + await inbox.OpenAsync (FolderAccess.ReadOnly); + + var messages = await inbox.FetchAsync (0, -1, MessageSummaryItems.UniqueId | MessageSummaryItems.BodyStructure); + using (var stream = await inbox.GetStreamAsync (messages[0].UniqueId, messages[0].TextBody.PartSpecifier + ".TEXT", 0, 512)) { + var text = Encoding.UTF8.GetString (((MemoryStream) stream).ToArray ()); + + Assert.That (text, Is.EqualTo (string.Empty), "The message body does not match."); + } + + await client.DisconnectAsync (false); + } + } + + static List CreateExpungeDuringFetchCommands () + { + return new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt"), + new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "gmail.authenticate.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-inbox.txt"), + new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt"), + new ImapReplayCommand ("A00000005 LIST \"\" \"%\"\r\n", "gmail.list-personal.txt"), + new ImapReplayCommand ("A00000006 EXAMINE INBOX (CONDSTORE)\r\n", "gmail.examine-inbox.txt"), + new ImapReplayCommand ("A00000007 UID FETCH 1:6 (UID INTERNALDATE ENVELOPE)\r\n", "gmail.expunge-during-fetch.txt") + }; + } + + [Test] + public void TestExpungeDuringFetch () + { + var commands = CreateExpungeDuringFetchCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: Do not try XOAUTH2 + client.AuthenticationMechanisms.Remove ("XOAUTH2"); + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + // disable LIST-EXTENDED + client.Capabilities &= ~ImapCapabilities.ListExtended; + + var personal = client.GetFolder (client.PersonalNamespaces[0]); + var folders = personal.GetSubfolders (); + Assert.That (folders[0], Is.EqualTo (client.Inbox), "Expected the first folder to be the Inbox."); + Assert.That (folders[1].FullName, Is.EqualTo ("[Gmail]"), "Expected the second folder to be [Gmail]."); + Assert.That (folders[1].Attributes, Is.EqualTo (FolderAttributes.NoSelect | FolderAttributes.HasChildren), "Expected [Gmail] folder to be \\Noselect \\HasChildren."); + + var inbox = client.Inbox; + + inbox.Open (FolderAccess.ReadOnly); + + var range = new UniqueIdRange (0, 1, 6); + var messages = inbox.Fetch (range, MessageSummaryItems.UniqueId | MessageSummaryItems.InternalDate | MessageSummaryItems.Envelope); + + Assert.That (messages, Has.Count.EqualTo (4), "Count"); + for (int i = 0; i < messages.Count; i++) + Assert.That (messages[i].Index, Is.EqualTo (i), $"Index #{i}"); + Assert.That (messages[0].UniqueId.Id, Is.EqualTo ((uint) 1), "UniqueId #0"); + Assert.That (messages[1].UniqueId.Id, Is.EqualTo ((uint) 3), "UniqueId #1"); + Assert.That (messages[2].UniqueId.Id, Is.EqualTo ((uint) 4), "UniqueId #2"); + Assert.That (messages[3].UniqueId.Id, Is.EqualTo ((uint) 5), "UniqueId #3"); + + client.Disconnect (false); + } + } + + [Test] + public async Task TestExpungeDuringFetchAsync () + { + var commands = CreateExpungeDuringFetchCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: Do not try XOAUTH2 + client.AuthenticationMechanisms.Remove ("XOAUTH2"); + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + // disable LIST-EXTENDED + client.Capabilities &= ~ImapCapabilities.ListExtended; + + var personal = client.GetFolder (client.PersonalNamespaces[0]); + var folders = await personal.GetSubfoldersAsync (); + Assert.That (folders[0], Is.EqualTo (client.Inbox), "Expected the first folder to be the Inbox."); + Assert.That (folders[1].FullName, Is.EqualTo ("[Gmail]"), "Expected the second folder to be [Gmail]."); + Assert.That (folders[1].Attributes, Is.EqualTo (FolderAttributes.NoSelect | FolderAttributes.HasChildren), "Expected [Gmail] folder to be \\Noselect \\HasChildren."); + + var inbox = client.Inbox; + + await inbox.OpenAsync (FolderAccess.ReadOnly); + + var range = new UniqueIdRange (0, 1, 6); + var messages = await inbox.FetchAsync (range, MessageSummaryItems.UniqueId | MessageSummaryItems.InternalDate | MessageSummaryItems.Envelope); + + Assert.That (messages, Has.Count.EqualTo (4), "Count"); + for (int i = 0; i < messages.Count; i++) + Assert.That (messages[i].Index, Is.EqualTo (i), $"Index #{i}"); + Assert.That (messages[0].UniqueId.Id, Is.EqualTo ((uint) 1), "UniqueId #0"); + Assert.That (messages[1].UniqueId.Id, Is.EqualTo ((uint) 3), "UniqueId #1"); + Assert.That (messages[2].UniqueId.Id, Is.EqualTo ((uint) 4), "UniqueId #2"); + Assert.That (messages[3].UniqueId.Id, Is.EqualTo ((uint) 5), "UniqueId #3"); + + await client.DisconnectAsync (false); + } + } + + static List CreateExtractingPrecisePangolinAttachmentCommands () + { + return new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt"), + new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "gmail.authenticate.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-inbox.txt"), + new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt"), + new ImapReplayCommand ("A00000005 LIST \"\" \"%\"\r\n", "gmail.list-personal.txt"), + new ImapReplayCommand ("A00000006 EXAMINE INBOX (CONDSTORE)\r\n", "gmail.examine-inbox.txt"), + new ImapReplayCommand ("A00000007 FETCH 270 (BODY.PEEK[])\r\n", "gmail.precise-pangolin-message.txt") + }; + } + + [Test] + public void TestExtractingPrecisePangolinAttachment () + { + var commands = CreateExtractingPrecisePangolinAttachmentCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: Do not try XOAUTH2 + client.AuthenticationMechanisms.Remove ("XOAUTH2"); + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + var inbox = client.Inbox; + Assert.That (inbox, Is.Not.Null, "Expected non-null Inbox folder."); + Assert.That (inbox.Attributes, Is.EqualTo (FolderAttributes.Inbox | FolderAttributes.HasNoChildren | FolderAttributes.Subscribed), "Expected Inbox attributes to be \\HasNoChildren."); + + foreach (var special in Enum.GetValues (typeof (SpecialFolder)).OfType ()) { + var folder = client.GetFolder (special); + + if (special != SpecialFolder.Archive) { + var expected = GetSpecialFolderAttribute (special) | FolderAttributes.HasNoChildren; + + Assert.That (folder, Is.Not.Null, $"Expected non-null {special} folder."); + Assert.That (folder.Attributes, Is.EqualTo (expected), $"Expected {special} attributes to be \\HasNoChildren."); + } else { + Assert.That (folder, Is.Null, $"Expected null {special} folder."); + } + } + + // disable LIST-EXTENDED + client.Capabilities &= ~ImapCapabilities.ListExtended; + + var personal = client.GetFolder (client.PersonalNamespaces[0]); + var folders = personal.GetSubfolders (); + Assert.That (folders[0], Is.EqualTo (client.Inbox), "Expected the first folder to be the Inbox."); + Assert.That (folders[1].FullName, Is.EqualTo ("[Gmail]"), "Expected the second folder to be [Gmail]."); + Assert.That (folders[1].Attributes, Is.EqualTo (FolderAttributes.NoSelect | FolderAttributes.HasChildren), "Expected [Gmail] folder to be \\Noselect \\HasChildren."); + + client.Inbox.Open (FolderAccess.ReadOnly); + + using (var message = client.Inbox.GetMessage (269)) { + using (var jpeg = new MemoryStream ()) { + var attachment = message.Attachments.OfType ().FirstOrDefault (); + + attachment.Content.DecodeTo (jpeg); + jpeg.Position = 0; + + using (var md5 = MD5.Create ()) { + var md5sum = HexEncode (md5.ComputeHash (jpeg)); + + Assert.That (md5sum, Is.EqualTo ("167a46aa81e881da2ea8a840727384d3"), "MD5 checksums do not match."); + } + } + } + + client.Disconnect (false); + } + } + + [Test] + public async Task TestExtractingPrecisePangolinAttachmentAsync () + { + var commands = CreateExtractingPrecisePangolinAttachmentCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: Do not try XOAUTH2 + client.AuthenticationMechanisms.Remove ("XOAUTH2"); + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + var inbox = client.Inbox; + Assert.That (inbox, Is.Not.Null, "Expected non-null Inbox folder."); + Assert.That (inbox.Attributes, Is.EqualTo (FolderAttributes.Inbox | FolderAttributes.HasNoChildren | FolderAttributes.Subscribed), "Expected Inbox attributes to be \\HasNoChildren."); + + foreach (var special in Enum.GetValues (typeof (SpecialFolder)).OfType ()) { + var folder = client.GetFolder (special); + + if (special != SpecialFolder.Archive) { + var expected = GetSpecialFolderAttribute (special) | FolderAttributes.HasNoChildren; + + Assert.That (folder, Is.Not.Null, $"Expected non-null {special} folder."); + Assert.That (folder.Attributes, Is.EqualTo (expected), $"Expected {special} attributes to be \\HasNoChildren."); + } else { + Assert.That (folder, Is.Null, $"Expected null {special} folder."); + } + } + + // disable LIST-EXTENDED + client.Capabilities &= ~ImapCapabilities.ListExtended; + + var personal = client.GetFolder (client.PersonalNamespaces[0]); + var folders = await personal.GetSubfoldersAsync (); + Assert.That (folders[0], Is.EqualTo (client.Inbox), "Expected the first folder to be the Inbox."); + Assert.That (folders[1].FullName, Is.EqualTo ("[Gmail]"), "Expected the second folder to be [Gmail]."); + Assert.That (folders[1].Attributes, Is.EqualTo (FolderAttributes.NoSelect | FolderAttributes.HasChildren), "Expected [Gmail] folder to be \\Noselect \\HasChildren."); + + await client.Inbox.OpenAsync (FolderAccess.ReadOnly); + + using (var message = await client.Inbox.GetMessageAsync (269)) { + using (var jpeg = new MemoryStream ()) { + var attachment = message.Attachments.OfType ().FirstOrDefault (); + + attachment.Content.DecodeTo (jpeg); + jpeg.Position = 0; + + using (var md5 = MD5.Create ()) { + var md5sum = HexEncode (md5.ComputeHash (jpeg)); + + Assert.That (md5sum, Is.EqualTo ("167a46aa81e881da2ea8a840727384d3"), "MD5 checksums do not match."); + } + } + } + + await client.DisconnectAsync (false); + } + } + + static List CreateFetchObjectIdAttributesCommands () + { + return new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt"), + new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "gmail.authenticate+statussize+objectid.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-inbox.txt"), + new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt"), + new ImapReplayCommand ("A00000005 EXAMINE INBOX (CONDSTORE)\r\n", "gmail.examine-inbox.txt"), + new ImapReplayCommand ("A00000006 FETCH 1:* (UID EMAILID THREADID)\r\n", "gmail.fetch-objectid.txt"), + new ImapReplayCommand ("A00000007 LOGOUT\r\n", "gmail.logout.txt") + }; + } + + [Test] + public void TestFetchObjectIdAttributes () + { + var commands = CreateFetchObjectIdAttributesCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities.HasFlag (ImapCapabilities.ObjectID), Is.True, "OBJECTID"); + + var inbox = client.Inbox; + inbox.Open (FolderAccess.ReadOnly); + + var messages = inbox.Fetch (0, -1, MessageSummaryItems.UniqueId | MessageSummaryItems.EmailId | MessageSummaryItems.ThreadId); + Assert.That (messages, Has.Count.EqualTo (4), "Count"); + Assert.That (messages[0].UniqueId.Id, Is.EqualTo (1), "UniqueId"); + Assert.That (messages[0].EmailId, Is.EqualTo ("M6d99ac3275bb4e"), "EmailId"); + Assert.That (messages[0].ThreadId, Is.EqualTo ("T64b478a75b7ea9"), "ThreadId"); + Assert.That (messages[1].UniqueId.Id, Is.EqualTo (2), "UniqueId"); + Assert.That (messages[1].EmailId, Is.EqualTo ("M288836c4c7a762"), "EmailId"); + Assert.That (messages[1].ThreadId, Is.EqualTo ("T64b478a75b7ea9"), "ThreadId"); + Assert.That (messages[2].UniqueId.Id, Is.EqualTo (3), "UniqueId"); + Assert.That (messages[2].EmailId, Is.EqualTo ("M5fdc09b49ea703"), "EmailId"); + Assert.That (messages[2].ThreadId, Is.EqualTo ("T11863d02dd95b5"), "ThreadId"); + Assert.That (messages[3].UniqueId.Id, Is.EqualTo (4), "UniqueId"); + Assert.That (messages[3].EmailId, Is.EqualTo ("M4fdc09b49ea629"), "EmailId"); + Assert.That (messages[3].ThreadId, Is.EqualTo (null), "ThreadId"); + + client.Disconnect (true); + } + } + + [Test] + public async Task TestFetchObjectIdAttributesAsync () + { + var commands = CreateFetchObjectIdAttributesCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities.HasFlag (ImapCapabilities.ObjectID), Is.True, "OBJECTID"); + + var inbox = client.Inbox; + await inbox.OpenAsync (FolderAccess.ReadOnly); + + var messages = await inbox.FetchAsync (0, -1, MessageSummaryItems.UniqueId | MessageSummaryItems.EmailId | MessageSummaryItems.ThreadId); + Assert.That (messages, Has.Count.EqualTo (4), "Count"); + Assert.That (messages[0].UniqueId.Id, Is.EqualTo (1), "UniqueId"); + Assert.That (messages[0].EmailId, Is.EqualTo ("M6d99ac3275bb4e"), "EmailId"); + Assert.That (messages[0].ThreadId, Is.EqualTo ("T64b478a75b7ea9"), "ThreadId"); + Assert.That (messages[1].UniqueId.Id, Is.EqualTo (2), "UniqueId"); + Assert.That (messages[1].EmailId, Is.EqualTo ("M288836c4c7a762"), "EmailId"); + Assert.That (messages[1].ThreadId, Is.EqualTo ("T64b478a75b7ea9"), "ThreadId"); + Assert.That (messages[2].UniqueId.Id, Is.EqualTo (3), "UniqueId"); + Assert.That (messages[2].EmailId, Is.EqualTo ("M5fdc09b49ea703"), "EmailId"); + Assert.That (messages[2].ThreadId, Is.EqualTo ("T11863d02dd95b5"), "ThreadId"); + Assert.That (messages[3].UniqueId.Id, Is.EqualTo (4), "UniqueId"); + Assert.That (messages[3].EmailId, Is.EqualTo ("M4fdc09b49ea629"), "EmailId"); + Assert.That (messages[3].ThreadId, Is.EqualTo (null), "ThreadId"); + + await client.DisconnectAsync (true); + } + } + + static List CreateFetchSaveDateCommands () + { + return new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt"), + new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "gmail.authenticate+savedate.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-inbox.txt"), + new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt"), + new ImapReplayCommand ("A00000005 EXAMINE INBOX (CONDSTORE)\r\n", "gmail.examine-inbox.txt"), + new ImapReplayCommand ("A00000006 FETCH 1:* (UID SAVEDATE)\r\n", "gmail.fetch-savedate.txt"), + new ImapReplayCommand ("A00000007 LOGOUT\r\n", "gmail.logout.txt") + }; + } + + [Test] + public void TestFetchSaveDate () + { + var commands = CreateFetchSaveDateCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities.HasFlag (ImapCapabilities.SaveDate), Is.True, "SAVEDATE"); + + var inbox = client.Inbox; + inbox.Open (FolderAccess.ReadOnly); + + var messages = inbox.Fetch (0, -1, MessageSummaryItems.UniqueId | MessageSummaryItems.SaveDate); + var dto = new DateTimeOffset (2023, 9, 12, 13, 39, 01, new TimeSpan (-4, 0, 0)); + + Assert.That (messages, Has.Count.EqualTo (4), "Count"); + Assert.That (messages[0].UniqueId.Id, Is.EqualTo (1), "UniqueId"); + Assert.That (messages[0].SaveDate, Is.EqualTo (dto), "SaveDate"); + Assert.That (messages[1].UniqueId.Id, Is.EqualTo (2), "UniqueId"); + Assert.That (messages[1].SaveDate, Is.EqualTo (dto), "SaveDate"); + Assert.That (messages[2].UniqueId.Id, Is.EqualTo (3), "UniqueId"); + Assert.That (messages[2].SaveDate, Is.EqualTo (dto), "SaveDate"); + Assert.That (messages[3].UniqueId.Id, Is.EqualTo (4), "UniqueId"); + Assert.That (messages[3].SaveDate, Is.EqualTo (null), "SaveDate"); + + client.Disconnect (true); + } + } + + [Test] + public async Task TestFetchSaveDateAsync () + { + var commands = CreateFetchSaveDateCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities.HasFlag (ImapCapabilities.SaveDate), Is.True, "SAVEDATE"); + + var inbox = client.Inbox; + await inbox.OpenAsync (FolderAccess.ReadOnly); + + var messages = await inbox.FetchAsync (0, -1, MessageSummaryItems.UniqueId | MessageSummaryItems.SaveDate); + var dto = new DateTimeOffset (2023, 9, 12, 13, 39, 01, new TimeSpan (-4, 0, 0)); + + Assert.That (messages, Has.Count.EqualTo (4), "Count"); + Assert.That (messages[0].UniqueId.Id, Is.EqualTo (1), "UniqueId"); + Assert.That (messages[0].SaveDate, Is.EqualTo (dto), "SaveDate"); + Assert.That (messages[1].UniqueId.Id, Is.EqualTo (2), "UniqueId"); + Assert.That (messages[1].SaveDate, Is.EqualTo (dto), "SaveDate"); + Assert.That (messages[2].UniqueId.Id, Is.EqualTo (3), "UniqueId"); + Assert.That (messages[2].SaveDate, Is.EqualTo (dto), "SaveDate"); + Assert.That (messages[3].UniqueId.Id, Is.EqualTo (4), "UniqueId"); + Assert.That (messages[3].SaveDate, Is.EqualTo (null), "SaveDate"); + + await client.DisconnectAsync (true); + } + } + + static List CreateFetchAnnotationsCommands () + { + return new List { + new ImapReplayCommand ("", "dovecot.greeting.txt"), + new ImapReplayCommand ("A00000000 LOGIN username password\r\n", "dovecot.authenticate+annotate.txt"), + new ImapReplayCommand ("A00000001 NAMESPACE\r\n", "dovecot.namespace.txt"), + new ImapReplayCommand ("A00000002 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-inbox.txt"), + new ImapReplayCommand ("A00000003 LIST (SPECIAL-USE) \"\" \"*\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-special-use.txt"), + new ImapReplayCommand ("A00000004 SELECT INBOX (CONDSTORE ANNOTATE)\r\n", "common.select-inbox-annotate-readonly.txt"), + new ImapReplayCommand ("A00000005 FETCH 1:* (UID ANNOTATION (/* (value size)))\r\n", "common.fetch-annotations.txt"), + new ImapReplayCommand ("A00000006 NOOP\r\n", "common.fetch-annotations.txt"), + }; + } + + [Test] + public void TestFetchAnnotations () + { + var commands = CreateFetchAnnotationsCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities.HasFlag (ImapCapabilities.Annotate), Is.True, "ANNOTATE-EXPERIMENT-1"); + + var inbox = client.Inbox; + inbox.Open (FolderAccess.ReadWrite); + + Assert.That (inbox.AnnotationAccess, Is.EqualTo (AnnotationAccess.ReadOnly), "AnnotationAccess"); + //Assert.That (inbox.AnnotationScopes, Is.EqualTo (AnnotationScope.Shared), "AnnotationScopes"); + //Assert.That (inbox.MaxAnnotationSize, Is.EqualTo (20480), "MaxAnnotationSize"); + + var messages = inbox.Fetch (0, -1, MessageSummaryItems.UniqueId | MessageSummaryItems.Annotations); + Assert.That (messages, Has.Count.EqualTo (3), "Count"); + + IReadOnlyList annotations; + + Assert.That (messages[0].UniqueId.Id, Is.EqualTo (1), "UniqueId"); + annotations = messages[0].Annotations; + Assert.That (annotations, Has.Count.EqualTo (1), "Count"); + Assert.That (annotations[0].Entry, Is.EqualTo (AnnotationEntry.Comment), "Entry"); + Assert.That (annotations[0].Properties, Has.Count.EqualTo (2), "Properties.Count"); + Assert.That (annotations[0].Properties[AnnotationAttribute.PrivateValue], Is.EqualTo ("My comment"), "value.priv"); + Assert.That (annotations[0].Properties[AnnotationAttribute.SharedValue], Is.EqualTo (null), "value.shared"); + + Assert.That (messages[1].UniqueId.Id, Is.EqualTo (2), "UniqueId"); + annotations = messages[1].Annotations; + Assert.That (annotations, Has.Count.EqualTo (2), "Count"); + Assert.That (annotations[0].Entry, Is.EqualTo (AnnotationEntry.Comment), "annotations[0].Entry"); + Assert.That (annotations[0].Properties, Has.Count.EqualTo (2), "annotations[0].Properties.Count"); + Assert.That (annotations[0].Properties[AnnotationAttribute.PrivateValue], Is.EqualTo ("My comment"), "annotations[0] value.priv"); + Assert.That (annotations[0].Properties[AnnotationAttribute.SharedValue], Is.EqualTo (null), "annotations[0] value.shared"); + Assert.That (annotations[1].Entry, Is.EqualTo (AnnotationEntry.AltSubject), "annotations[1].Entry"); + Assert.That (annotations[1].Properties, Has.Count.EqualTo (2), "annotations[1].Properties.Count"); + Assert.That (annotations[1].Properties[AnnotationAttribute.PrivateValue], Is.EqualTo ("My subject"), "annotations[1] value.priv"); + Assert.That (annotations[1].Properties[AnnotationAttribute.SharedValue], Is.EqualTo (null), "annotations[1] value.shared"); + + Assert.That (messages[2].UniqueId.Id, Is.EqualTo (3), "UniqueId"); + annotations = messages[2].Annotations; + Assert.That (annotations, Has.Count.EqualTo (1), "Count"); + Assert.That (annotations[0].Entry, Is.EqualTo (AnnotationEntry.Comment), "annotations[0].Entry"); + Assert.That (annotations[0].Properties, Has.Count.EqualTo (4), "annotations[0].Properties.Count"); + Assert.That (annotations[0].Properties[AnnotationAttribute.PrivateValue], Is.EqualTo ("My comment"), "annotations[0] value.priv"); + Assert.That (annotations[0].Properties[AnnotationAttribute.SharedValue], Is.EqualTo (null), "annotations[0] value.shared"); + Assert.That (annotations[0].Properties[AnnotationAttribute.PrivateSize], Is.EqualTo ("10"), "annotations[0] size.priv"); + Assert.That (annotations[0].Properties[AnnotationAttribute.SharedSize], Is.EqualTo ("0"), "annotations[0] size.shared"); + + var annotationsChanged = new List (); + + inbox.AnnotationsChanged += (sender, e) => { + annotationsChanged.Add (e); + }; + + client.NoOp (); + + Assert.That (annotationsChanged, Has.Count.EqualTo (3), "# AnnotationsChanged events"); + + client.Disconnect (false); + } + } + + [Test] + public async Task TestFetchAnnotationsAsync () + { + var commands = CreateFetchAnnotationsCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities.HasFlag (ImapCapabilities.Annotate), Is.True, "ANNOTATE-EXPERIMENT-1"); + + var inbox = client.Inbox; + await inbox.OpenAsync (FolderAccess.ReadWrite); + + Assert.That (inbox.AnnotationAccess, Is.EqualTo (AnnotationAccess.ReadOnly), "AnnotationAccess"); + //Assert.That (inbox.AnnotationScopes, Is.EqualTo (AnnotationScope.Shared), "AnnotationScopes"); + //Assert.That (inbox.MaxAnnotationSize, Is.EqualTo (20480), "MaxAnnotationSize"); + + var messages = await inbox.FetchAsync (0, -1, MessageSummaryItems.UniqueId | MessageSummaryItems.Annotations); + Assert.That (messages, Has.Count.EqualTo (3), "Count"); + + IReadOnlyList annotations; + + Assert.That (messages[0].UniqueId.Id, Is.EqualTo (1), "UniqueId"); + annotations = messages[0].Annotations; + Assert.That (annotations, Has.Count.EqualTo (1), "Count"); + Assert.That (annotations[0].Entry, Is.EqualTo (AnnotationEntry.Comment), "Entry"); + Assert.That (annotations[0].Properties, Has.Count.EqualTo (2), "Properties.Count"); + Assert.That (annotations[0].Properties[AnnotationAttribute.PrivateValue], Is.EqualTo ("My comment"), "value.priv"); + Assert.That (annotations[0].Properties[AnnotationAttribute.SharedValue], Is.EqualTo (null), "value.shared"); + + Assert.That (messages[1].UniqueId.Id, Is.EqualTo (2), "UniqueId"); + annotations = messages[1].Annotations; + Assert.That (annotations, Has.Count.EqualTo (2), "Count"); + Assert.That (annotations[0].Entry, Is.EqualTo (AnnotationEntry.Comment), "annotations[0].Entry"); + Assert.That (annotations[0].Properties, Has.Count.EqualTo (2), "annotations[0].Properties.Count"); + Assert.That (annotations[0].Properties[AnnotationAttribute.PrivateValue], Is.EqualTo ("My comment"), "annotations[0] value.priv"); + Assert.That (annotations[0].Properties[AnnotationAttribute.SharedValue], Is.EqualTo (null), "annotations[0] value.shared"); + Assert.That (annotations[1].Entry, Is.EqualTo (AnnotationEntry.AltSubject), "annotations[1].Entry"); + Assert.That (annotations[1].Properties, Has.Count.EqualTo (2), "annotations[1].Properties.Count"); + Assert.That (annotations[1].Properties[AnnotationAttribute.PrivateValue], Is.EqualTo ("My subject"), "annotations[1] value.priv"); + Assert.That (annotations[1].Properties[AnnotationAttribute.SharedValue], Is.EqualTo (null), "annotations[1] value.shared"); + + Assert.That (messages[2].UniqueId.Id, Is.EqualTo (3), "UniqueId"); + annotations = messages[2].Annotations; + Assert.That (annotations, Has.Count.EqualTo (1), "Count"); + Assert.That (annotations[0].Entry, Is.EqualTo (AnnotationEntry.Comment), "annotations[0].Entry"); + Assert.That (annotations[0].Properties, Has.Count.EqualTo (4), "annotations[0].Properties.Count"); + Assert.That (annotations[0].Properties[AnnotationAttribute.PrivateValue], Is.EqualTo ("My comment"), "annotations[0] value.priv"); + Assert.That (annotations[0].Properties[AnnotationAttribute.SharedValue], Is.EqualTo (null), "annotations[0] value.shared"); + Assert.That (annotations[0].Properties[AnnotationAttribute.PrivateSize], Is.EqualTo ("10"), "annotations[0] size.priv"); + Assert.That (annotations[0].Properties[AnnotationAttribute.SharedSize], Is.EqualTo ("0"), "annotations[0] size.shared"); + + var annotationsChanged = new List (); + + inbox.AnnotationsChanged += (sender, e) => { + annotationsChanged.Add (e); + }; + + await client.NoOpAsync (); + + Assert.That (annotationsChanged, Has.Count.EqualTo (3), "# AnnotationsChanged events"); + + client.Disconnect (false); + } + } + + static List CreateDominoParenthesisWorkaroundCommands () + { + return new List { + new ImapReplayCommand ("", Encoding.ASCII.GetBytes ("* OK Domino IMAP4 Server Release 10.0.1FP3 ready Wed, 30 Oct 2019 09:28:06 +0100\r\n")), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "domino.capability.txt"), + new ImapReplayCommand ("A00000001 LOGIN username password\r\n", ImapReplayCommandResponse.OK), + new ImapReplayCommand ("A00000002 CAPABILITY\r\n", "domino.capability.txt"), + new ImapReplayCommand ("A00000003 NAMESPACE\r\n", "domino.namespace.txt"), + new ImapReplayCommand ("A00000004 LIST \"\" \"INBOX\"\r\n", "domino.list-inbox.txt"), + new ImapReplayCommand ("A00000005 SELECT Inbox\r\n", "common.select-inbox.txt"), + new ImapReplayCommand ("A00000006 FETCH 1:* (UID ENVELOPE BODYSTRUCTURE)\r\n", "domino.fetch-extra-parens.txt") + }; + } + + [Test] + public void TestDominoParenthesisWorkaround () + { + var commands = CreateDominoParenthesisWorkaroundCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.PersonalNamespaces, Has.Count.EqualTo (1), "Personal Count"); + Assert.That (client.PersonalNamespaces[0].Path, Is.EqualTo (""), "Personal Path"); + Assert.That (client.PersonalNamespaces[0].DirectorySeparator, Is.EqualTo ('\\'), "Personal DirectorySeparator"); + + Assert.That (client.OtherNamespaces, Has.Count.EqualTo (1), "Other Count"); + Assert.That (client.OtherNamespaces[0].Path, Is.EqualTo ("Other"), "Other Path"); + Assert.That (client.OtherNamespaces[0].DirectorySeparator, Is.EqualTo ('\\'), "Other DirectorySeparator"); + + Assert.That (client.SharedNamespaces, Has.Count.EqualTo (1), "Shared Count"); + Assert.That (client.SharedNamespaces[0].Path, Is.EqualTo ("Shared"), "Shared Path"); + Assert.That (client.SharedNamespaces[0].DirectorySeparator, Is.EqualTo ('\\'), "Shared DirectorySeparator"); + + var inbox = client.Inbox; + inbox.Open (FolderAccess.ReadWrite); + + var messages = inbox.Fetch (0, -1, MessageSummaryItems.UniqueId | MessageSummaryItems.Envelope | MessageSummaryItems.BodyStructure); + Assert.That (messages, Has.Count.EqualTo (29), "Count"); + + for (int i = 0; i < 29; i++) { + Assert.That (messages[i].Index, Is.EqualTo (i), "MessageSummaryItems are out of order!"); + } + + client.Disconnect (false); + } + } + + [Test] + public async Task TestDominoParenthesisWorkaroundAsync () + { + var commands = CreateDominoParenthesisWorkaroundCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.PersonalNamespaces, Has.Count.EqualTo (1), "Personal Count"); + Assert.That (client.PersonalNamespaces[0].Path, Is.EqualTo (""), "Personal Path"); + Assert.That (client.PersonalNamespaces[0].DirectorySeparator, Is.EqualTo ('\\'), "Personal DirectorySeparator"); + + Assert.That (client.OtherNamespaces, Has.Count.EqualTo (1), "Other Count"); + Assert.That (client.OtherNamespaces[0].Path, Is.EqualTo ("Other"), "Other Path"); + Assert.That (client.OtherNamespaces[0].DirectorySeparator, Is.EqualTo ('\\'), "Other DirectorySeparator"); + + Assert.That (client.SharedNamespaces, Has.Count.EqualTo (1), "Shared Count"); + Assert.That (client.SharedNamespaces[0].Path, Is.EqualTo ("Shared"), "Shared Path"); + Assert.That (client.SharedNamespaces[0].DirectorySeparator, Is.EqualTo ('\\'), "Shared DirectorySeparator"); + + var inbox = client.Inbox; + await inbox.OpenAsync (FolderAccess.ReadWrite); + + var messages = await inbox.FetchAsync (0, -1, MessageSummaryItems.UniqueId | MessageSummaryItems.Envelope | MessageSummaryItems.BodyStructure); + Assert.That (messages, Has.Count.EqualTo (29), "Count"); + + for (int i = 0; i < 29; i++) { + Assert.That (messages[i].Index, Is.EqualTo (i), "MessageSummaryItems are out of order!"); + } + + client.Disconnect (false); + } + } + + static IList CreateFetchStreamUnsolicitedInfoCommands () + { + return new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt"), + new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "gmail.authenticate+annotate.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-inbox.txt"), + new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt"), + new ImapReplayCommand ("A00000005 SELECT INBOX (CONDSTORE ANNOTATE)\r\n", "common.select-inbox-annotate.txt"), + new ImapReplayCommand ("A00000006 UID FETCH 1 (BODY.PEEK[HEADER])\r\n", "gmail.headers.1+unsolicited-info.txt"), + new ImapReplayCommand ("A00000007 UID FETCH 1 (BODY.PEEK[])\r\n", "gmail.fetch.1+unsolicited-info.txt"), + new ImapReplayCommand ("A00000008 UID FETCH 1 (BODY.PEEK[])\r\n", "gmail.fetch.1+unsolicited-info.txt") + }; + } + + [Test] + public void TestFetchStreamUnsolicitedInfo () + { + var commands = CreateFetchStreamUnsolicitedInfoCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities.HasFlag (ImapCapabilities.Annotate), Is.True, "ANNOTATE-EXPERIMENT-1"); + + var inbox = client.Inbox; + inbox.Open (FolderAccess.ReadWrite); + + Assert.That (inbox.AnnotationAccess, Is.EqualTo (AnnotationAccess.ReadWrite), "AnnotationAccess"); + //Assert.That (inbox.AnnotationScopes, Is.EqualTo (AnnotationScope.Shared), "AnnotationScopes"); + //Assert.That (inbox.MaxAnnotationSize, Is.EqualTo (20480), "MaxAnnotationSize"); + + // Keep track of various folder events + var annotationsChanged = new List (); + var flagsChanged = new List (); + var labelsChanged = new List (); + var modSeqChanged = new List (); + + inbox.AnnotationsChanged += (sender, e) => { + annotationsChanged.Add (e); + }; + + inbox.MessageFlagsChanged += (sender, e) => { + flagsChanged.Add (e); + }; + + inbox.MessageLabelsChanged += (sender, e) => { + labelsChanged.Add (e); + }; + + inbox.ModSeqChanged += (sender, e) => { + modSeqChanged.Add (e); + }; + + Assert.That (inbox.HighestModSeq, Is.EqualTo (2), "HIGHESTMODSEQ #1"); + + var headers = inbox.GetHeaders (new UniqueId (1)); + var message = inbox.GetMessage (new UniqueId (1)); + var stream = inbox.GetStream (new UniqueId (1)); + + Assert.That (inbox.HighestModSeq, Is.EqualTo (29233), "HIGHESTMODSEQ #2"); + + Assert.That (annotationsChanged, Has.Count.EqualTo (3), "AnnotationsChanged"); + Assert.That (flagsChanged, Has.Count.EqualTo (3), "FlagsChanged"); + Assert.That (labelsChanged, Has.Count.EqualTo (3), "LabelsChanged"); + Assert.That (modSeqChanged, Has.Count.EqualTo (3), "ModSeqChanged"); + + client.Disconnect (false); + } + } + + [Test] + public async Task TestFetchStreamUnsolicitedInfoAsync () + { + var commands = CreateFetchStreamUnsolicitedInfoCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities.HasFlag (ImapCapabilities.Annotate), Is.True, "ANNOTATE-EXPERIMENT-1"); + + var inbox = client.Inbox; + await inbox.OpenAsync (FolderAccess.ReadWrite); + + Assert.That (inbox.AnnotationAccess, Is.EqualTo (AnnotationAccess.ReadWrite), "AnnotationAccess"); + //Assert.That (inbox.AnnotationScopes, Is.EqualTo (AnnotationScope.Shared), "AnnotationScopes"); + //Assert.That (inbox.MaxAnnotationSize, Is.EqualTo (20480), "MaxAnnotationSize"); + + // Keep track of various folder events + var annotationsChanged = new List (); + var flagsChanged = new List (); + var labelsChanged = new List (); + var modSeqChanged = new List (); + + inbox.AnnotationsChanged += (sender, e) => { + annotationsChanged.Add (e); + }; + + inbox.MessageFlagsChanged += (sender, e) => { + flagsChanged.Add (e); + }; + + inbox.MessageLabelsChanged += (sender, e) => { + labelsChanged.Add (e); + }; + + inbox.ModSeqChanged += (sender, e) => { + modSeqChanged.Add (e); + }; + + Assert.That (inbox.HighestModSeq, Is.EqualTo (2), "HIGHESTMODSEQ #1"); + + var headers = await inbox.GetHeadersAsync (new UniqueId (1)); + var message = await inbox.GetMessageAsync (new UniqueId (1)); + var stream = await inbox.GetStreamAsync (new UniqueId (1)); + + Assert.That (inbox.HighestModSeq, Is.EqualTo (29233), "HIGHESTMODSEQ #2"); + + Assert.That (annotationsChanged, Has.Count.EqualTo (3), "AnnotationsChanged"); + Assert.That (flagsChanged, Has.Count.EqualTo (3), "FlagsChanged"); + Assert.That (labelsChanged, Has.Count.EqualTo (3), "LabelsChanged"); + Assert.That (modSeqChanged, Has.Count.EqualTo (3), "ModSeqChanged"); + + client.Disconnect (false); + } + } + + static IList CreateFetchNegativeModSeqResponseValuesCommands () + { + return new List { + new ImapReplayCommand ("", "zoho.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "zoho.capability.txt"), + new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "zoho.authenticate.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "zoho.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "zoho.list-inbox.txt"), + new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "zoho.xlist.txt"), + new ImapReplayCommand ("A00000005 EXAMINE Gesendet (CONDSTORE)\r\n", "zoho.examine-gesendet.txt"), + new ImapReplayCommand ("A00000006 FETCH 1:74 (UID FLAGS MODSEQ)\r\n", "zoho.fetch-negative-modseq-values.txt") + }; + } + + [Test] + public void TestFetchNegativeModSeqResponseValues () + { + var commands = CreateFetchNegativeModSeqResponseValuesCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + var gesendet = client.GetFolder (SpecialFolder.Sent); + gesendet.Open (FolderAccess.ReadOnly); + + var messages = gesendet.Fetch (0, 73, MessageSummaryItems.UniqueId | MessageSummaryItems.Flags | MessageSummaryItems.ModSeq); + Assert.That (messages, Has.Count.EqualTo (74), "Count"); + + for (int i = 0; i < 15; i++) { + Assert.That (messages[i].ModSeq, Is.EqualTo ((ulong) 0), $"MODSEQ {i}"); + Assert.That (messages[i].Flags, Is.EqualTo (MessageFlags.Seen), $"FLAGS {i}"); + } + + for (int i = 15; i < 39; i++) { + Assert.That (messages[i].ModSeq, Is.EqualTo ((ulong) 0), $"MODSEQ {i}"); + Assert.That (messages[i].Flags, Is.EqualTo (MessageFlags.Seen | MessageFlags.Recent), $"FLAGS {i}"); + + if (i == 35) { + Assert.That (messages[i].Keywords, Has.Count.EqualTo (1), $"KEYWORDS {i}"); + Assert.That (messages[i].Keywords.Contains ("$FORWARDED"), Is.True, $"KEYWORDS {i}"); + } + } + + for (int i = 39; i < 70; i++) { + if (i == 53) { + Assert.That (messages[i].ModSeq, Is.EqualTo (1538484935027010002), $"MODSEQ {i}"); + Assert.That (messages[i].Flags, Is.EqualTo (MessageFlags.Seen | MessageFlags.Recent | MessageFlags.Answered), $"FLAGS {i}"); + } else { + Assert.That (messages[i].ModSeq, Is.Null, $"MODSEQ {i}"); + Assert.That (messages[i].Flags, Is.EqualTo (MessageFlags.Seen | MessageFlags.Recent), $"FLAGS {i}"); + } + } + } + } + + [Test] + public async Task TestFetchNegativeModSeqResponseValuesAsync () + { + var commands = CreateFetchNegativeModSeqResponseValuesCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + var gesendet = client.GetFolder (SpecialFolder.Sent); + await gesendet.OpenAsync (FolderAccess.ReadOnly); + + var messages = await gesendet.FetchAsync (0, 73, MessageSummaryItems.UniqueId | MessageSummaryItems.Flags | MessageSummaryItems.ModSeq); + Assert.That (messages, Has.Count.EqualTo (74), "Count"); + + for (int i = 0; i < 15; i++) { + Assert.That (messages[i].ModSeq, Is.EqualTo ((ulong) 0), $"MODSEQ {i}"); + Assert.That (messages[i].Flags, Is.EqualTo (MessageFlags.Seen), $"FLAGS {i}"); + } + + for (int i = 15; i < 39; i++) { + Assert.That (messages[i].ModSeq, Is.EqualTo ((ulong) 0), $"MODSEQ {i}"); + Assert.That (messages[i].Flags, Is.EqualTo (MessageFlags.Seen | MessageFlags.Recent), $"FLAGS {i}"); + + if (i == 35) { + Assert.That (messages[i].Keywords, Has.Count.EqualTo (1), $"KEYWORDS {i}"); + Assert.That (messages[i].Keywords.Contains ("$FORWARDED"), Is.True, $"KEYWORDS {i}"); + } + } + + for (int i = 39; i < 70; i++) { + if (i == 53) { + Assert.That (messages[i].ModSeq, Is.EqualTo (1538484935027010002), $"MODSEQ {i}"); + Assert.That (messages[i].Flags, Is.EqualTo (MessageFlags.Seen | MessageFlags.Recent | MessageFlags.Answered), $"FLAGS {i}"); + } else { + Assert.That (messages[i].ModSeq, Is.Null, $"MODSEQ {i}"); + Assert.That (messages[i].Flags, Is.EqualTo (MessageFlags.Seen | MessageFlags.Recent), $"FLAGS {i}"); + } + } + } + } + + static IList CreateYandexGetBodyPartMissingContentCommands () + { + return new List { + new ImapReplayCommand ("", "yandex.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "yandex.capability.txt"), + new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN\r\n", ImapReplayCommandResponse.Plus), + new ImapReplayCommand ("A00000001", "AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "yandex.authenticate.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "yandex.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\"\r\n", "yandex.list-inbox.txt"), + new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "yandex.xlist.txt"), + new ImapReplayCommand ("A00000005 SELECT INBOX\r\n", "yandex.select-inbox.txt"), + new ImapReplayCommand ("A00000006 UID FETCH 3016 (BODY.PEEK[2.MIME] BODY.PEEK[2])\r\n", "yandex.getbodypart-missing-content.txt") + }; + } + + [Test] + public void TestYandexGetBodyPartMissingContent () + { + // IMAP4rev1 CHILDREN UNSELECT LITERAL+ NAMESPACE XLIST UIDPLUS ENABLE ID AUTH=PLAIN AUTH=XOAUTH2 IDLE MOVE + const ImapCapabilities YandexGreetingCapabilities = ImapCapabilities.IMAP4rev1 | ImapCapabilities.Children | ImapCapabilities.Unselect | + ImapCapabilities.LiteralPlus | ImapCapabilities.Namespace | ImapCapabilities.XList | ImapCapabilities.UidPlus | ImapCapabilities.Enable | + ImapCapabilities.Id | ImapCapabilities.Idle | ImapCapabilities.Move | ImapCapabilities.Status; + var commands = CreateYandexGetBodyPartMissingContentCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.Capabilities, Is.EqualTo (YandexGreetingCapabilities), "Greeting Capabilities"); + Assert.That (client.AuthenticationMechanisms, Has.Count.EqualTo (2)); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN"), "Expected SASL PLAIN auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH2"), "Expected SASL XOAUTH2 auth mechanism"); + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities, Is.EqualTo (YandexGreetingCapabilities), "Greeting Capabilities"); + + client.Inbox.Open (FolderAccess.ReadWrite); + + //var messages = client.Inbox.Fetch (0, -1, MessageSummaryItems.UniqueId | MessageSummaryItems.Flags | MessageSummaryItems.ModSeq); + //Assert.That (messages, Has.Count.EqualTo (74), "Count"); + + var bodyPart = new BodyPartBasic (new ContentType ("application", "pdf"), "2"); + var body = client.Inbox.GetBodyPart (new UniqueId (3016), bodyPart); + Assert.That (body, Is.Not.Null); + Assert.That (body, Is.InstanceOf ()); + var part = (MimePart) body; + Assert.That (part.ContentType.MimeType, Is.EqualTo ("application/pdf"), "Content-Type"); + Assert.That (part.ContentType.Name, Is.EqualTo ("empty.pdf"), "name"); + Assert.That (part.ContentDisposition.Disposition, Is.EqualTo (ContentDisposition.Attachment), "Content-Disposition"); + Assert.That (part.ContentDisposition.FileName, Is.EqualTo ("empty.pdf"), "filename"); + Assert.That (part.ContentTransferEncoding, Is.EqualTo (ContentEncoding.Base64), "Content-Transfer-Encoding"); + Assert.That (part.Content, Is.Null); + } + } + + [Test] + public async Task TestYandexGetBodyPartMissingContentAsync () + { + // IMAP4rev1 CHILDREN UNSELECT LITERAL+ NAMESPACE XLIST UIDPLUS ENABLE ID AUTH=PLAIN AUTH=XOAUTH2 IDLE MOVE + const ImapCapabilities YandexGreetingCapabilities = ImapCapabilities.IMAP4rev1 | ImapCapabilities.Children | ImapCapabilities.Unselect | + ImapCapabilities.LiteralPlus | ImapCapabilities.Namespace | ImapCapabilities.XList | ImapCapabilities.UidPlus | ImapCapabilities.Enable | + ImapCapabilities.Id | ImapCapabilities.Idle | ImapCapabilities.Move | ImapCapabilities.Status; + var commands = CreateYandexGetBodyPartMissingContentCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.Capabilities, Is.EqualTo (YandexGreetingCapabilities), "Greeting Capabilities"); + Assert.That (client.AuthenticationMechanisms, Has.Count.EqualTo (2)); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN"), "Expected SASL PLAIN auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH2"), "Expected SASL XOAUTH2 auth mechanism"); + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities, Is.EqualTo (YandexGreetingCapabilities), "Greeting Capabilities"); + + await client.Inbox.OpenAsync (FolderAccess.ReadWrite); + + //var messages = client.Inbox.Fetch (0, -1, MessageSummaryItems.UniqueId | MessageSummaryItems.Flags | MessageSummaryItems.ModSeq); + //Assert.That (messages, Has.Count.EqualTo (74), "Count"); + + var bodyPart = new BodyPartBasic (new ContentType ("application", "pdf"), "2"); + var body = await client.Inbox.GetBodyPartAsync (new UniqueId (3016), bodyPart); + Assert.That (body, Is.Not.Null); + Assert.That (body, Is.InstanceOf ()); + var part = (MimePart) body; + Assert.That (part.ContentType.MimeType, Is.EqualTo ("application/pdf"), "Content-Type"); + Assert.That (part.ContentType.Name, Is.EqualTo ("empty.pdf"), "name"); + Assert.That (part.ContentDisposition.Disposition, Is.EqualTo (ContentDisposition.Attachment), "Content-Disposition"); + Assert.That (part.ContentDisposition.FileName, Is.EqualTo ("empty.pdf"), "filename"); + Assert.That (part.ContentTransferEncoding, Is.EqualTo (ContentEncoding.Base64), "Content-Transfer-Encoding"); + Assert.That (part.Content, Is.Null); + } + } + + [TestCase ("ALL", MessageSummaryItems.All)] + [TestCase ("FAST", MessageSummaryItems.Fast)] + [TestCase ("FULL", MessageSummaryItems.Full)] + public void TestFormatFetchSummaryItemsMacros (string expected, MessageSummaryItems items) + { + using (var engine = new ImapEngine (null)) { + var request = new FetchRequest (items); + var command = ImapFolder.FormatSummaryItems (engine, request, out _); + + Assert.That (command, Is.EqualTo (expected)); + } + } + + [Test] + public void TestFormatFetchSummaryItemsExcludeHeaders () + { + using (var engine = new ImapEngine (null)) { + var request = new FetchRequest () { + Headers = new HeaderSet () { + Exclude = true + } + }; + string command; + + command = ImapFolder.FormatSummaryItems (engine, request, out _); + Assert.That (command, Is.EqualTo ("BODY.PEEK[HEADER]")); + + request = new FetchRequest () { + Headers = new HeaderSet (new[] { "FROM", "SUBJECT", "DATE" }) { + Exclude = true + } + }; + + command = ImapFolder.FormatSummaryItems (engine, request, out _); + Assert.That (command, Is.EqualTo ("BODY.PEEK[HEADER.FIELDS.NOT (FROM SUBJECT DATE)]")); + } + } + + [Test] + public void TestFormatFetchSummaryItemsReferences () + { + using (var engine = new ImapEngine (null)) { + var request = new FetchRequest (MessageSummaryItems.References); + string command; + + command = ImapFolder.FormatSummaryItems (engine, request, out _); + Assert.That (command, Is.EqualTo ("BODY.PEEK[HEADER.FIELDS (REFERENCES)]")); + + request = new FetchRequest () { + Headers = new HeaderSet (new[] { "REFERENCES" }) + }; + + command = ImapFolder.FormatSummaryItems (engine, request, out _); + Assert.That (command, Is.EqualTo ("BODY.PEEK[HEADER.FIELDS (REFERENCES)]")); + + request = new FetchRequest (MessageSummaryItems.References) { + Headers = new HeaderSet (new[] { "REFERENCES" }) + }; + + command = ImapFolder.FormatSummaryItems (engine, request, out _); + Assert.That (command, Is.EqualTo ("BODY.PEEK[HEADER.FIELDS (REFERENCES)]")); + } + } + + [Test] + public void TestFormatFetchSummaryItemsAllHeaders () + { + using (var engine = new ImapEngine (null)) { + var request = new FetchRequest (MessageSummaryItems.Headers); + + var command = ImapFolder.FormatSummaryItems (engine, request, out _); + Assert.That (command, Is.EqualTo ("BODY.PEEK[HEADER]")); + } + } + + [Test] + public void TestFormatFetchSummaryItemsHeaderFieldsAndReferences () + { + using (var engine = new ImapEngine (null)) { + var request = new FetchRequest (MessageSummaryItems.References) { + Headers = new HeaderSet (new[] { HeaderId.InReplyTo }) + }; + + var command = ImapFolder.FormatSummaryItems (engine, request, out _); + Assert.That (command, Is.EqualTo ("BODY.PEEK[HEADER.FIELDS (IN-REPLY-TO REFERENCES)]")); + } + } + + [Test] + public void TestFormatFetchSummaryItemsExcludeHeaderFieldsReferencesAndReferences () + { + using (var engine = new ImapEngine (null)) { + var request = new FetchRequest (MessageSummaryItems.References) { + Headers = new HeaderSet (new[] { HeaderId.References }) { + Exclude = true + } + }; + + var command = ImapFolder.FormatSummaryItems (engine, request, out _); + Assert.That (command, Is.EqualTo ("BODY.PEEK[HEADER]")); + } + } + + [Test] + public void TestFormatFetchSummaryItemsExcludeHeaderFieldsInReplyToAndReferences () + { + using (var engine = new ImapEngine (null)) { + var request = new FetchRequest (MessageSummaryItems.References) { + Headers = new HeaderSet (new[] { HeaderId.InReplyTo }) { + Exclude = true + } + }; + + var command = ImapFolder.FormatSummaryItems (engine, request, out _); + Assert.That (command, Is.EqualTo ("BODY.PEEK[HEADER.FIELDS.NOT (IN-REPLY-TO)]")); + } + } + + [Test] + public void TestFormatFetchSummaryItemsExcludeHeaderFieldsInReplyToReferencesAndReferences () + { + using (var engine = new ImapEngine (null)) { + var request = new FetchRequest (MessageSummaryItems.References) { + Headers = new HeaderSet (new[] { HeaderId.InReplyTo, HeaderId.References }) { + Exclude = true + } + }; + + var command = ImapFolder.FormatSummaryItems (engine, request, out _); + Assert.That (command, Is.EqualTo ("BODY.PEEK[HEADER.FIELDS.NOT (IN-REPLY-TO)]")); + } + } + } +} diff --git a/UnitTests/Net/Imap/ImapFolderFlagsTests.cs b/UnitTests/Net/Imap/ImapFolderFlagsTests.cs new file mode 100644 index 0000000000..b4a32736ca --- /dev/null +++ b/UnitTests/Net/Imap/ImapFolderFlagsTests.cs @@ -0,0 +1,545 @@ +// +// ImapFolderFlagsTests.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System.Net; + +using MailKit; +using MailKit.Security; +using MailKit.Net.Imap; + +namespace UnitTests.Net.Imap { + [TestFixture] + public class ImapFolderFlagsTests + { + [Test] + public void TestArgumentExceptions () + { + var keywords = new HashSet (new string[] { "$Forwarded", "$Junk" }); + var commands = new List { + new ImapReplayCommand ("", "dovecot.greeting.txt"), + new ImapReplayCommand ("A00000000 LOGIN username password\r\n", "dovecot.authenticate+gmail-capabilities.txt"), + new ImapReplayCommand ("A00000001 NAMESPACE\r\n", "dovecot.namespace.txt"), + new ImapReplayCommand ("A00000002 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-inbox.txt"), + new ImapReplayCommand ("A00000003 LIST (SPECIAL-USE) \"\" \"*\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-special-use.txt"), + new ImapReplayCommand ("A00000004 SELECT INBOX (CONDSTORE)\r\n", "common.select-inbox.txt") + }; + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + var credentials = new NetworkCredential ("username", "password"); + + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + client.Authenticate (credentials); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Inbox.SyncRoot, Is.InstanceOf (), "SyncRoot"); + + var inbox = (ImapFolder) client.Inbox; + inbox.Open (FolderAccess.ReadWrite); + + // AddFlags + Assert.Throws (() => inbox.AddFlags (-1, MessageFlags.Seen, true)); + Assert.ThrowsAsync (() => inbox.AddFlagsAsync (-1, MessageFlags.Seen, true)); + Assert.Throws (() => inbox.AddFlags (UniqueId.Invalid, MessageFlags.Seen, true)); + Assert.ThrowsAsync (() => inbox.AddFlagsAsync (UniqueId.Invalid, MessageFlags.Seen, true)); + Assert.Throws (() => inbox.AddFlags (-1, MessageFlags.Seen, keywords, true)); + Assert.ThrowsAsync (() => inbox.AddFlagsAsync (-1, MessageFlags.Seen, keywords, true)); + Assert.Throws (() => inbox.AddFlags (UniqueId.Invalid, MessageFlags.Seen, keywords, true)); + Assert.ThrowsAsync (() => inbox.AddFlagsAsync (UniqueId.Invalid, MessageFlags.Seen, keywords, true)); + Assert.Throws (() => inbox.AddFlags ((IList) null, MessageFlags.Seen, true)); + Assert.ThrowsAsync (() => inbox.AddFlagsAsync ((IList) null, MessageFlags.Seen, true)); + Assert.Throws (() => inbox.AddFlags ((IList) null, MessageFlags.Seen, true)); + Assert.ThrowsAsync (() => inbox.AddFlagsAsync ((IList) null, MessageFlags.Seen, true)); + Assert.Throws (() => inbox.AddFlags ((IList) null, MessageFlags.Seen, keywords, true)); + Assert.ThrowsAsync (() => inbox.AddFlagsAsync ((IList) null, MessageFlags.Seen, keywords, true)); + Assert.Throws (() => inbox.AddFlags ((IList) null, MessageFlags.Seen, keywords, true)); + Assert.ThrowsAsync (() => inbox.AddFlagsAsync ((IList) null, MessageFlags.Seen, keywords, true)); + Assert.Throws (() => inbox.AddFlags ((IList) null, 1, MessageFlags.Seen, true)); + Assert.ThrowsAsync (() => inbox.AddFlagsAsync ((IList) null, 1, MessageFlags.Seen, true)); + Assert.Throws (() => inbox.AddFlags ((IList) null, 1, MessageFlags.Seen, true)); + Assert.ThrowsAsync (() => inbox.AddFlagsAsync ((IList) null, 1, MessageFlags.Seen, true)); + Assert.Throws (() => inbox.AddFlags ((IList) null, 1, MessageFlags.Seen, keywords, true)); + Assert.ThrowsAsync (() => inbox.AddFlagsAsync ((IList) null, 1, MessageFlags.Seen, keywords, true)); + Assert.Throws (() => inbox.AddFlags ((IList) null, 1, MessageFlags.Seen, keywords, true)); + Assert.ThrowsAsync (() => inbox.AddFlagsAsync ((IList) null, 1, MessageFlags.Seen, keywords, true)); + + // RemoveFlags + Assert.Throws (() => inbox.RemoveFlags (-1, MessageFlags.Seen, true)); + Assert.ThrowsAsync (() => inbox.RemoveFlagsAsync (-1, MessageFlags.Seen, true)); + Assert.Throws (() => inbox.RemoveFlags (UniqueId.Invalid, MessageFlags.Seen, true)); + Assert.ThrowsAsync (() => inbox.RemoveFlagsAsync (UniqueId.Invalid, MessageFlags.Seen, true)); + Assert.Throws (() => inbox.RemoveFlags (-1, MessageFlags.Seen, keywords, true)); + Assert.ThrowsAsync (() => inbox.RemoveFlagsAsync (-1, MessageFlags.Seen, keywords, true)); + Assert.Throws (() => inbox.RemoveFlags (UniqueId.Invalid, MessageFlags.Seen, keywords, true)); + Assert.ThrowsAsync (() => inbox.RemoveFlagsAsync (UniqueId.Invalid, MessageFlags.Seen, keywords, true)); + Assert.Throws (() => inbox.RemoveFlags ((IList) null, MessageFlags.Seen, true)); + Assert.ThrowsAsync (() => inbox.RemoveFlagsAsync ((IList) null, MessageFlags.Seen, true)); + Assert.Throws (() => inbox.RemoveFlags ((IList) null, MessageFlags.Seen, true)); + Assert.ThrowsAsync (() => inbox.RemoveFlagsAsync ((IList) null, MessageFlags.Seen, true)); + Assert.Throws (() => inbox.RemoveFlags ((IList) null, MessageFlags.Seen, keywords, true)); + Assert.ThrowsAsync (() => inbox.RemoveFlagsAsync ((IList) null, MessageFlags.Seen, keywords, true)); + Assert.Throws (() => inbox.RemoveFlags ((IList) null, MessageFlags.Seen, keywords, true)); + Assert.ThrowsAsync (() => inbox.RemoveFlagsAsync ((IList) null, MessageFlags.Seen, keywords, true)); + Assert.Throws (() => inbox.RemoveFlags ((IList) null, 1, MessageFlags.Seen, true)); + Assert.ThrowsAsync (() => inbox.RemoveFlagsAsync ((IList) null, 1, MessageFlags.Seen, true)); + Assert.Throws (() => inbox.RemoveFlags ((IList) null, 1, MessageFlags.Seen, true)); + Assert.ThrowsAsync (() => inbox.RemoveFlagsAsync ((IList) null, 1, MessageFlags.Seen, true)); + Assert.Throws (() => inbox.RemoveFlags ((IList) null, 1, MessageFlags.Seen, keywords, true)); + Assert.ThrowsAsync (() => inbox.RemoveFlagsAsync ((IList) null, 1, MessageFlags.Seen, keywords, true)); + Assert.Throws (() => inbox.RemoveFlags ((IList) null, 1, MessageFlags.Seen, keywords, true)); + Assert.ThrowsAsync (() => inbox.RemoveFlagsAsync ((IList) null, 1, MessageFlags.Seen, keywords, true)); + + // SetFlags + Assert.Throws (() => inbox.SetFlags (-1, MessageFlags.Seen, true)); + Assert.ThrowsAsync (() => inbox.SetFlagsAsync (-1, MessageFlags.Seen, true)); + Assert.Throws (() => inbox.SetFlags (UniqueId.Invalid, MessageFlags.Seen, true)); + Assert.ThrowsAsync (() => inbox.SetFlagsAsync (UniqueId.Invalid, MessageFlags.Seen, true)); + Assert.Throws (() => inbox.SetFlags (-1, MessageFlags.Seen, keywords, true)); + Assert.ThrowsAsync (() => inbox.SetFlagsAsync (-1, MessageFlags.Seen, keywords, true)); + Assert.Throws (() => inbox.SetFlags (UniqueId.Invalid, MessageFlags.Seen, keywords, true)); + Assert.ThrowsAsync (() => inbox.SetFlagsAsync (UniqueId.Invalid, MessageFlags.Seen, keywords, true)); + Assert.Throws (() => inbox.SetFlags ((IList) null, MessageFlags.Seen, true)); + Assert.ThrowsAsync (() => inbox.SetFlagsAsync ((IList) null, MessageFlags.Seen, true)); + Assert.Throws (() => inbox.SetFlags ((IList) null, MessageFlags.Seen, true)); + Assert.ThrowsAsync (() => inbox.SetFlagsAsync ((IList) null, MessageFlags.Seen, true)); + Assert.Throws (() => inbox.SetFlags ((IList) null, MessageFlags.Seen, keywords, true)); + Assert.ThrowsAsync (() => inbox.SetFlagsAsync ((IList) null, MessageFlags.Seen, keywords, true)); + Assert.Throws (() => inbox.SetFlags ((IList) null, MessageFlags.Seen, keywords, true)); + Assert.ThrowsAsync (() => inbox.SetFlagsAsync ((IList) null, MessageFlags.Seen, keywords, true)); + Assert.Throws (() => inbox.SetFlags ((IList) null, 1, MessageFlags.Seen, true)); + Assert.ThrowsAsync (() => inbox.SetFlagsAsync ((IList) null, 1, MessageFlags.Seen, true)); + Assert.Throws (() => inbox.SetFlags ((IList) null, 1, MessageFlags.Seen, true)); + Assert.ThrowsAsync (() => inbox.SetFlagsAsync ((IList) null, 1, MessageFlags.Seen, true)); + Assert.Throws (() => inbox.SetFlags ((IList) null, 1, MessageFlags.Seen, keywords, true)); + Assert.ThrowsAsync (() => inbox.SetFlagsAsync ((IList) null, 1, MessageFlags.Seen, keywords, true)); + Assert.Throws (() => inbox.SetFlags ((IList) null, 1, MessageFlags.Seen, keywords, true)); + Assert.ThrowsAsync (() => inbox.SetFlagsAsync ((IList) null, 1, MessageFlags.Seen, keywords, true)); + + // Store Flags + var addSeen = new StoreFlagsRequest (StoreAction.Add, MessageFlags.Seen) { Silent = true }; + Assert.Throws (() => inbox.Store (UniqueId.Invalid, addSeen)); + Assert.ThrowsAsync (() => inbox.StoreAsync (UniqueId.Invalid, addSeen)); + Assert.Throws (() => inbox.Store (UniqueId.MinValue, (StoreFlagsRequest) null)); + Assert.ThrowsAsync (() => inbox.StoreAsync (UniqueId.MinValue, (StoreFlagsRequest) null)); + Assert.Throws (() => inbox.Store (-1, addSeen)); + Assert.ThrowsAsync (() => inbox.StoreAsync (-1, addSeen)); + Assert.Throws (() => inbox.Store (0, (StoreFlagsRequest) null)); + Assert.ThrowsAsync (() => inbox.StoreAsync (0, (StoreFlagsRequest) null)); + Assert.Throws (() => inbox.Store ((IList) null, addSeen)); + Assert.ThrowsAsync (() => inbox.StoreAsync ((IList) null, addSeen)); + Assert.Throws (() => inbox.Store (UniqueIdRange.All, (StoreFlagsRequest) null)); + Assert.ThrowsAsync (() => inbox.StoreAsync (UniqueIdRange.All, (StoreFlagsRequest) null)); + Assert.Throws (() => inbox.Store ((IList) null, addSeen)); + Assert.ThrowsAsync (() => inbox.StoreAsync ((IList) null, addSeen)); + Assert.Throws (() => inbox.Store (new int[] { 0 }, (StoreFlagsRequest) null)); + Assert.ThrowsAsync (() => inbox.StoreAsync (new int[] { 0 }, (StoreFlagsRequest) null)); + + var labels = new string [] { "Label1", "Label2" }; + var emptyLabels = Array.Empty (); + + // AddLabels + Assert.Throws (() => inbox.AddLabels (-1, labels, true)); + Assert.ThrowsAsync (() => inbox.AddLabelsAsync (-1, labels, true)); + Assert.Throws (() => inbox.AddLabels (0, null, true)); + Assert.ThrowsAsync (() => inbox.AddLabelsAsync (0, null, true)); + Assert.Throws (() => inbox.AddLabels (UniqueId.MinValue, null, true)); + Assert.ThrowsAsync (() => inbox.AddLabelsAsync (UniqueId.MinValue, null, true)); + Assert.Throws (() => inbox.AddLabels ((IList) null, labels, true)); + Assert.ThrowsAsync (() => inbox.AddLabelsAsync ((IList) null, labels, true)); + Assert.Throws (() => inbox.AddLabels ((IList) null, labels, true)); + Assert.ThrowsAsync (() => inbox.AddLabelsAsync ((IList) null, labels, true)); + Assert.Throws (() => inbox.AddLabels (new int [] { 0 }, null, true)); + Assert.ThrowsAsync (() => inbox.AddLabelsAsync (new int [] { 0 }, null, true)); + Assert.Throws (() => inbox.AddLabels (UniqueIdRange.All, null, true)); + Assert.ThrowsAsync (() => inbox.AddLabelsAsync (UniqueIdRange.All, null, true)); + + Assert.Throws (() => inbox.AddLabels ((IList) null, 1, labels, true)); + Assert.ThrowsAsync (() => inbox.AddLabelsAsync ((IList) null, 1, labels, true)); + Assert.Throws (() => inbox.AddLabels ((IList) null, 1, labels, true)); + Assert.ThrowsAsync (() => inbox.AddLabelsAsync ((IList) null, 1, labels, true)); + Assert.Throws (() => inbox.AddLabels (new int [] { 0 }, 1, null, true)); + Assert.ThrowsAsync (() => inbox.AddLabelsAsync (new int [] { 0 }, 1, null, true)); + Assert.Throws (() => inbox.AddLabels (UniqueIdRange.All, 1, null, true)); + Assert.ThrowsAsync (() => inbox.AddLabelsAsync (UniqueIdRange.All, 1, null, true)); + + // RemoveLabels + Assert.Throws (() => inbox.RemoveLabels (-1, labels, true)); + Assert.ThrowsAsync (() => inbox.RemoveLabelsAsync (-1, labels, true)); + Assert.Throws (() => inbox.RemoveLabels (0, null, true)); + Assert.ThrowsAsync (() => inbox.RemoveLabelsAsync (0, null, true)); + Assert.Throws (() => inbox.RemoveLabels (UniqueId.MinValue, null, true)); + Assert.ThrowsAsync (() => inbox.RemoveLabelsAsync (UniqueId.MinValue, null, true)); + Assert.Throws (() => inbox.RemoveLabels ((IList) null, labels, true)); + Assert.ThrowsAsync (() => inbox.RemoveLabelsAsync ((IList) null, labels, true)); + Assert.Throws (() => inbox.RemoveLabels ((IList) null, labels, true)); + Assert.ThrowsAsync (() => inbox.RemoveLabelsAsync ((IList) null, labels, true)); + Assert.Throws (() => inbox.RemoveLabels (new int [] { 0 }, null, true)); + Assert.ThrowsAsync (() => inbox.RemoveLabelsAsync (new int [] { 0 }, null, true)); + Assert.Throws (() => inbox.RemoveLabels (UniqueIdRange.All, null, true)); + Assert.ThrowsAsync (() => inbox.RemoveLabelsAsync (UniqueIdRange.All, null, true)); + + Assert.Throws (() => inbox.RemoveLabels ((IList) null, 1, labels, true)); + Assert.ThrowsAsync (() => inbox.RemoveLabelsAsync ((IList) null, 1, labels, true)); + Assert.Throws (() => inbox.RemoveLabels ((IList) null, 1, labels, true)); + Assert.ThrowsAsync (() => inbox.RemoveLabelsAsync ((IList) null, 1, labels, true)); + Assert.Throws (() => inbox.RemoveLabels (new int [] { 0 }, 1, null, true)); + Assert.ThrowsAsync (() => inbox.RemoveLabelsAsync (new int [] { 0 }, 1, null, true)); + Assert.Throws (() => inbox.RemoveLabels (UniqueIdRange.All, 1, null, true)); + Assert.ThrowsAsync (() => inbox.RemoveLabelsAsync (UniqueIdRange.All, 1, null, true)); + + // SetLabels + Assert.Throws (() => inbox.SetLabels (-1, labels, true)); + Assert.ThrowsAsync (() => inbox.SetLabelsAsync (-1, labels, true)); + Assert.Throws (() => inbox.SetLabels (0, null, true)); + Assert.ThrowsAsync (() => inbox.SetLabelsAsync (0, null, true)); + Assert.Throws (() => inbox.SetLabels (UniqueId.MinValue, null, true)); + Assert.ThrowsAsync (() => inbox.SetLabelsAsync (UniqueId.MinValue, null, true)); + Assert.Throws (() => inbox.SetLabels ((IList) null, labels, true)); + Assert.ThrowsAsync (() => inbox.SetLabelsAsync ((IList) null, labels, true)); + Assert.Throws (() => inbox.SetLabels ((IList) null, labels, true)); + Assert.ThrowsAsync (() => inbox.SetLabelsAsync ((IList) null, labels, true)); + Assert.Throws (() => inbox.SetLabels (new int [] { 0 }, null, true)); + Assert.ThrowsAsync (() => inbox.SetLabelsAsync (new int [] { 0 }, null, true)); + Assert.Throws (() => inbox.SetLabels (UniqueIdRange.All, null, true)); + Assert.ThrowsAsync (() => inbox.SetLabelsAsync (UniqueIdRange.All, null, true)); + + Assert.Throws (() => inbox.SetLabels ((IList) null, 1, labels, true)); + Assert.ThrowsAsync (() => inbox.SetLabelsAsync ((IList) null, 1, labels, true)); + Assert.Throws (() => inbox.SetLabels ((IList) null, 1, labels, true)); + Assert.ThrowsAsync (() => inbox.SetLabelsAsync ((IList) null, 1, labels, true)); + Assert.Throws (() => inbox.SetLabels (new int [] { 0 }, 1, null, true)); + Assert.ThrowsAsync (() => inbox.SetLabelsAsync (new int [] { 0 }, 1, null, true)); + Assert.Throws (() => inbox.SetLabels (UniqueIdRange.All, 1, null, true)); + Assert.ThrowsAsync (() => inbox.SetLabelsAsync (UniqueIdRange.All, 1, null, true)); + + // Store Labels + var addLabel = new StoreLabelsRequest (StoreAction.Add, new string[] { "Label1" }) { Silent = true }; + Assert.Throws (() => inbox.Store (UniqueId.Invalid, addLabel)); + Assert.ThrowsAsync (() => inbox.StoreAsync (UniqueId.Invalid, addLabel)); + Assert.Throws (() => inbox.Store (UniqueId.MinValue, (StoreLabelsRequest) null)); + Assert.ThrowsAsync (() => inbox.StoreAsync (UniqueId.MinValue, (StoreLabelsRequest) null)); + Assert.Throws (() => inbox.Store (-1, addLabel)); + Assert.ThrowsAsync (() => inbox.StoreAsync (-1, addLabel)); + Assert.Throws (() => inbox.Store (0, (StoreLabelsRequest) null)); + Assert.ThrowsAsync (() => inbox.StoreAsync (0, (StoreLabelsRequest) null)); + Assert.Throws (() => inbox.Store ((IList) null, addLabel)); + Assert.ThrowsAsync (() => inbox.StoreAsync ((IList) null, addLabel)); + Assert.Throws (() => inbox.Store (UniqueIdRange.All, (StoreLabelsRequest) null)); + Assert.ThrowsAsync (() => inbox.StoreAsync (UniqueIdRange.All, (StoreLabelsRequest) null)); + Assert.Throws (() => inbox.Store ((IList) null, addLabel)); + Assert.ThrowsAsync (() => inbox.StoreAsync ((IList) null, addLabel)); + Assert.Throws (() => inbox.Store (new int[] { 0 }, (StoreLabelsRequest) null)); + Assert.ThrowsAsync (() => inbox.StoreAsync (new int[] { 0 }, (StoreLabelsRequest) null)); + + client.Disconnect (false); + } + } + + [Test] + public void TestNotSupportedExceptions () + { + var commands = new List { + new ImapReplayCommand ("", "dovecot.greeting.txt"), + new ImapReplayCommand ("A00000000 LOGIN username password\r\n", "dovecot.authenticate+gmail-capabilities.txt"), + new ImapReplayCommand ("A00000001 NAMESPACE\r\n", "dovecot.namespace.txt"), + new ImapReplayCommand ("A00000002 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-inbox.txt"), + new ImapReplayCommand ("A00000003 LIST (SPECIAL-USE) \"\" \"*\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-special-use.txt"), + new ImapReplayCommand ("A00000004 SELECT INBOX\r\n", "common.select-inbox-no-modseq.txt") + }; + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + var credentials = new NetworkCredential ("username", "password"); + + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + client.Authenticate (credentials); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Inbox.SyncRoot, Is.InstanceOf (), "SyncRoot"); + + // disable all features + client.Capabilities = ImapCapabilities.None; + + var inbox = (ImapFolder) client.Inbox; + inbox.Open (FolderAccess.ReadWrite); + + var indexes = new int[] { 0 }; + ulong modseq = 409601020304; + + // AddFlags + Assert.Throws (() => inbox.AddFlags (indexes, modseq, MessageFlags.Seen, true)); + Assert.ThrowsAsync (() => inbox.AddFlagsAsync (indexes, modseq, MessageFlags.Seen, true)); + Assert.Throws (() => inbox.AddFlags (UniqueIdRange.All, modseq, MessageFlags.Seen, true)); + Assert.ThrowsAsync (() => inbox.AddFlagsAsync (UniqueIdRange.All, modseq, MessageFlags.Seen, true)); + + // RemoveFlags + Assert.Throws (() => inbox.RemoveFlags (indexes, modseq, MessageFlags.Seen, true)); + Assert.ThrowsAsync (() => inbox.RemoveFlagsAsync (indexes, modseq, MessageFlags.Seen, true)); + Assert.Throws (() => inbox.RemoveFlags (UniqueIdRange.All, modseq, MessageFlags.Seen, true)); + Assert.ThrowsAsync (() => inbox.RemoveFlagsAsync (UniqueIdRange.All, modseq, MessageFlags.Seen, true)); + + // SetFlags + Assert.Throws (() => inbox.SetFlags (indexes, modseq, MessageFlags.Seen, true)); + Assert.ThrowsAsync (() => inbox.SetFlagsAsync (indexes, modseq, MessageFlags.Seen, true)); + Assert.Throws (() => inbox.SetFlags (UniqueIdRange.All, modseq, MessageFlags.Seen, true)); + Assert.ThrowsAsync (() => inbox.SetFlagsAsync (UniqueIdRange.All, modseq, MessageFlags.Seen, true)); + + var labels = new string[] { "Label1", "Label2" }; + + // AddLabels + Assert.Throws (() => inbox.AddLabels (indexes, labels, true)); + Assert.ThrowsAsync (() => inbox.AddLabelsAsync (indexes, labels, true)); + Assert.Throws (() => inbox.AddLabels (UniqueIdRange.All, labels, true)); + Assert.ThrowsAsync (() => inbox.AddLabelsAsync (UniqueIdRange.All, labels, true)); + + Assert.Throws (() => inbox.AddLabels (indexes, modseq, labels, true)); + Assert.ThrowsAsync (() => inbox.AddLabelsAsync (indexes, modseq, labels, true)); + Assert.Throws (() => inbox.AddLabels (UniqueIdRange.All, modseq, labels, true)); + Assert.ThrowsAsync (() => inbox.AddLabelsAsync (UniqueIdRange.All, modseq, labels, true)); + + // RemoveLabels + Assert.Throws (() => inbox.RemoveLabels (indexes, labels, true)); + Assert.ThrowsAsync (() => inbox.RemoveLabelsAsync (indexes, labels, true)); + Assert.Throws (() => inbox.RemoveLabels (UniqueIdRange.All, labels, true)); + Assert.ThrowsAsync (() => inbox.RemoveLabelsAsync (UniqueIdRange.All, labels, true)); + + Assert.Throws (() => inbox.RemoveLabels (indexes, modseq, labels, true)); + Assert.ThrowsAsync (() => inbox.RemoveLabelsAsync (indexes, modseq, labels, true)); + Assert.Throws (() => inbox.RemoveLabels (UniqueIdRange.All, modseq, labels, true)); + Assert.ThrowsAsync (() => inbox.RemoveLabelsAsync (UniqueIdRange.All, modseq, labels, true)); + + // SetLabels + Assert.Throws (() => inbox.SetLabels (indexes, labels, true)); + Assert.ThrowsAsync (() => inbox.SetLabelsAsync (indexes, labels, true)); + Assert.Throws (() => inbox.SetLabels (UniqueIdRange.All, labels, true)); + Assert.ThrowsAsync (() => inbox.SetLabelsAsync (UniqueIdRange.All, labels, true)); + + Assert.Throws (() => inbox.SetLabels (indexes, modseq, labels, true)); + Assert.ThrowsAsync (() => inbox.SetLabelsAsync (indexes, modseq, labels, true)); + Assert.Throws (() => inbox.SetLabels (UniqueIdRange.All, modseq, labels, true)); + Assert.ThrowsAsync (() => inbox.SetLabelsAsync (UniqueIdRange.All, modseq, labels, true)); + + client.Disconnect (false); + } + } + + static IList CreateChangingFlagsOnEmptyListOfMessagesCommands () + { + return new List { + new ImapReplayCommand ("", "dovecot.greeting.txt"), + new ImapReplayCommand ("A00000000 LOGIN username password\r\n", "dovecot.authenticate+gmail-capabilities.txt"), + new ImapReplayCommand ("A00000001 NAMESPACE\r\n", "dovecot.namespace.txt"), + new ImapReplayCommand ("A00000002 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-inbox.txt"), + new ImapReplayCommand ("A00000003 LIST (SPECIAL-USE) \"\" \"*\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-special-use.txt"), + new ImapReplayCommand ("A00000004 SELECT INBOX (CONDSTORE)\r\n", "common.select-inbox.txt") + }; + } + + [Test] + public void TestChangingFlagsOnEmptyListOfMessages () + { + var commands = CreateChangingFlagsOnEmptyListOfMessagesCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + var credentials = new NetworkCredential ("username", "password"); + + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + client.Authenticate (credentials); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Inbox.SyncRoot, Is.InstanceOf (), "SyncRoot"); + + var inbox = (ImapFolder) client.Inbox; + inbox.Open (FolderAccess.ReadWrite); + + ulong modseq = 409601020304; + var uids = Array.Empty (); + var indexes = Array.Empty (); + IList unmodifiedUids; + IList unmodifiedIndexes; + + // AddFlags + unmodifiedIndexes = inbox.AddFlags (indexes, modseq, MessageFlags.Seen, true); + Assert.That (unmodifiedIndexes, Is.Empty); + + unmodifiedUids = inbox.AddFlags (uids, modseq, MessageFlags.Seen, true); + Assert.That (unmodifiedUids, Is.Empty); + + // RemoveFlags + unmodifiedIndexes = inbox.RemoveFlags (indexes, modseq, MessageFlags.Seen, true); + Assert.That (unmodifiedIndexes, Is.Empty); + + unmodifiedUids = inbox.RemoveFlags (uids, modseq, MessageFlags.Seen, true); + Assert.That (unmodifiedUids, Is.Empty); + + // SetFlags + unmodifiedIndexes = inbox.SetFlags (indexes, modseq, MessageFlags.Seen, true); + Assert.That (unmodifiedIndexes, Is.Empty); + + unmodifiedUids = inbox.SetFlags (uids, modseq, MessageFlags.Seen, true); + Assert.That (unmodifiedUids, Is.Empty); + + var labels = new string[] { "Label1", "Label2" }; + + // AddLabels + unmodifiedIndexes = inbox.AddLabels (indexes, modseq, labels, true); + Assert.That (unmodifiedIndexes, Is.Empty); + + unmodifiedUids = inbox.AddLabels (uids, modseq, labels, true); + Assert.That (unmodifiedUids, Is.Empty); + + // RemoveLabels + unmodifiedIndexes = inbox.RemoveLabels (indexes, modseq, labels, true); + Assert.That (unmodifiedIndexes, Is.Empty); + + unmodifiedUids = inbox.RemoveLabels (uids, modseq, labels, true); + Assert.That (unmodifiedUids, Is.Empty); + + // SetLabels + unmodifiedIndexes = inbox.SetLabels (indexes, modseq, labels, true); + Assert.That (unmodifiedIndexes, Is.Empty); + + unmodifiedUids = inbox.SetLabels (uids, modseq, labels, true); + Assert.That (unmodifiedUids, Is.Empty); + + client.Disconnect (false); + } + } + + [Test] + public async Task TestChangingFlagsOnEmptyListOfMessagesAsync () + { + var commands = CreateChangingFlagsOnEmptyListOfMessagesCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + var credentials = new NetworkCredential ("username", "password"); + + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + await client.AuthenticateAsync (credentials); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Inbox.SyncRoot, Is.InstanceOf (), "SyncRoot"); + + var inbox = (ImapFolder) client.Inbox; + await inbox.OpenAsync (FolderAccess.ReadWrite); + + ulong modseq = 409601020304; + var uids = Array.Empty (); + var indexes = Array.Empty (); + IList unmodifiedUids; + IList unmodifiedIndexes; + + // AddFlags + unmodifiedIndexes = await inbox.AddFlagsAsync (indexes, modseq, MessageFlags.Seen, true); + Assert.That (unmodifiedIndexes, Is.Empty); + + unmodifiedUids = await inbox.AddFlagsAsync (uids, modseq, MessageFlags.Seen, true); + Assert.That (unmodifiedUids, Is.Empty); + + // RemoveFlags + unmodifiedIndexes = await inbox.RemoveFlagsAsync (indexes, modseq, MessageFlags.Seen, true); + Assert.That (unmodifiedIndexes, Is.Empty); + + unmodifiedUids = await inbox.RemoveFlagsAsync (uids, modseq, MessageFlags.Seen, true); + Assert.That (unmodifiedUids, Is.Empty); + + // SetFlags + unmodifiedIndexes = await inbox.SetFlagsAsync (indexes, modseq, MessageFlags.Seen, true); + Assert.That (unmodifiedIndexes, Is.Empty); + + unmodifiedUids = await inbox.SetFlagsAsync (uids, modseq, MessageFlags.Seen, true); + Assert.That (unmodifiedUids, Is.Empty); + + var labels = new string[] { "Label1", "Label2" }; + + // AddLabels + unmodifiedIndexes = await inbox.AddLabelsAsync (indexes, modseq, labels, true); + Assert.That (unmodifiedIndexes, Is.Empty); + + unmodifiedUids = await inbox.AddLabelsAsync (uids, modseq, labels, true); + Assert.That (unmodifiedUids, Is.Empty); + + // RemoveLabels + unmodifiedIndexes = await inbox.RemoveLabelsAsync (indexes, modseq, labels, true); + Assert.That (unmodifiedIndexes, Is.Empty); + + unmodifiedUids = await inbox.RemoveLabelsAsync (uids, modseq, labels, true); + Assert.That (unmodifiedUids, Is.Empty); + + // SetLabels + unmodifiedIndexes = await inbox.SetLabelsAsync (indexes, modseq, labels, true); + Assert.That (unmodifiedIndexes, Is.Empty); + + unmodifiedUids = await inbox.SetLabelsAsync (uids, modseq, labels, true); + Assert.That (unmodifiedUids, Is.Empty); + + await client.DisconnectAsync (false); + } + } + } +} diff --git a/UnitTests/Net/Imap/ImapFolderSearchTests.cs b/UnitTests/Net/Imap/ImapFolderSearchTests.cs new file mode 100644 index 0000000000..63b6d1e68f --- /dev/null +++ b/UnitTests/Net/Imap/ImapFolderSearchTests.cs @@ -0,0 +1,1398 @@ +// +// ImapFolderSearchTests.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System.Net; +using System.Text; + +using MailKit; +using MailKit.Search; +using MailKit.Security; +using MailKit.Net.Imap; + +namespace UnitTests.Net.Imap { + [TestFixture] + public class ImapFolderSearchTests + { + [Test] + public void TestArgumentExceptions () + { + var commands = new List { + new ImapReplayCommand ("", "dovecot.greeting.txt"), + new ImapReplayCommand ("A00000000 LOGIN username password\r\n", "dovecot.authenticate+gmail-capabilities.txt"), + new ImapReplayCommand ("A00000001 NAMESPACE\r\n", "dovecot.namespace.txt"), + new ImapReplayCommand ("A00000002 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-inbox.txt"), + new ImapReplayCommand ("A00000003 LIST (SPECIAL-USE) \"\" \"*\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-special-use.txt"), + new ImapReplayCommand ("A00000004 SELECT INBOX (CONDSTORE)\r\n", "common.select-inbox.txt") + }; + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + var credentials = new NetworkCredential ("username", "password"); + + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + client.Authenticate (credentials); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Inbox.SyncRoot, Is.InstanceOf (), "SyncRoot"); + + var inbox = (ImapFolder) client.Inbox; + inbox.Open (FolderAccess.ReadWrite); + + // Search + var searchOptions = SearchOptions.All | SearchOptions.Min | SearchOptions.Max | SearchOptions.Count; + var orderBy = new OrderBy [] { OrderBy.Arrival }; + var emptyOrderBy = Array.Empty (); + + Assert.Throws (() => inbox.Search ((SearchQuery) null)); + Assert.ThrowsAsync (async () => await inbox.SearchAsync ((SearchQuery) null)); + Assert.Throws (() => inbox.Search ((IList) null, SearchQuery.All)); + Assert.ThrowsAsync (async () => await inbox.SearchAsync ((IList) null, SearchQuery.All)); + Assert.Throws (() => inbox.Search (UniqueIdRange.All, (SearchQuery) null)); + Assert.ThrowsAsync (async () => await inbox.SearchAsync (UniqueIdRange.All, (SearchQuery) null)); + Assert.Throws (() => inbox.Search (searchOptions, null)); + Assert.ThrowsAsync (async () => await inbox.SearchAsync (searchOptions, null)); + Assert.Throws (() => inbox.Search (searchOptions, (IList) null, SearchQuery.All)); + Assert.ThrowsAsync (async () => await inbox.SearchAsync (searchOptions, (IList) null, SearchQuery.All)); + Assert.Throws (() => inbox.Search (searchOptions, UniqueIdRange.All, (SearchQuery) null)); + Assert.ThrowsAsync (async () => await inbox.SearchAsync (searchOptions, UniqueIdRange.All, (SearchQuery) null)); + + Assert.Throws (() => inbox.Search ((string) null)); + Assert.ThrowsAsync (async () => await inbox.SearchAsync ((string) null)); + Assert.Throws (() => inbox.Search (string.Empty)); + Assert.ThrowsAsync (async () => await inbox.SearchAsync (string.Empty)); + + // Sort + Assert.Throws (() => inbox.Sort ((SearchQuery) null, orderBy)); + Assert.ThrowsAsync (async () => await inbox.SortAsync ((SearchQuery) null, orderBy)); + Assert.Throws (() => inbox.Sort (SearchQuery.All, null)); + Assert.ThrowsAsync (async () => await inbox.SortAsync (SearchQuery.All, null)); + Assert.Throws (() => inbox.Sort (SearchQuery.All, emptyOrderBy)); + Assert.ThrowsAsync (async () => await inbox.SortAsync (SearchQuery.All, emptyOrderBy)); + + Assert.Throws (() => inbox.Sort ((IList) null, SearchQuery.All, orderBy)); + Assert.ThrowsAsync (async () => await inbox.SortAsync ((IList) null, SearchQuery.All, orderBy)); + Assert.Throws (() => inbox.Sort (UniqueIdRange.All, (SearchQuery) null, orderBy)); + Assert.ThrowsAsync (async () => await inbox.SortAsync (UniqueIdRange.All, (SearchQuery) null, orderBy)); + Assert.Throws (() => inbox.Sort (UniqueIdRange.All, SearchQuery.All, null)); + Assert.ThrowsAsync (async () => await inbox.SortAsync (UniqueIdRange.All, SearchQuery.All, null)); + Assert.Throws (() => inbox.Sort (UniqueIdRange.All, SearchQuery.All, emptyOrderBy)); + Assert.ThrowsAsync (async () => await inbox.SortAsync (UniqueIdRange.All, SearchQuery.All, emptyOrderBy)); + + Assert.Throws (() => inbox.Sort (searchOptions, (SearchQuery) null, orderBy)); + Assert.ThrowsAsync (async () => await inbox.SortAsync (searchOptions, (SearchQuery) null, orderBy)); + Assert.Throws (() => inbox.Sort (searchOptions, SearchQuery.All, null)); + Assert.ThrowsAsync (async () => await inbox.SortAsync (searchOptions, SearchQuery.All, null)); + Assert.Throws (() => inbox.Sort (searchOptions, SearchQuery.All, emptyOrderBy)); + Assert.ThrowsAsync (async () => await inbox.SortAsync (searchOptions, SearchQuery.All, emptyOrderBy)); + + Assert.Throws (() => inbox.Sort (searchOptions, (IList) null, SearchQuery.All, orderBy)); + Assert.ThrowsAsync (async () => await inbox.SortAsync (searchOptions, (IList) null, SearchQuery.All, orderBy)); + Assert.Throws (() => inbox.Sort (searchOptions, UniqueIdRange.All, (SearchQuery) null, orderBy)); + Assert.ThrowsAsync (async () => await inbox.SortAsync (searchOptions, UniqueIdRange.All, (SearchQuery) null, orderBy)); + Assert.Throws (() => inbox.Sort (searchOptions, UniqueIdRange.All, SearchQuery.All, null)); + Assert.ThrowsAsync (async () => await inbox.SortAsync (searchOptions, UniqueIdRange.All, SearchQuery.All, null)); + Assert.Throws (() => inbox.Sort (searchOptions, UniqueIdRange.All, SearchQuery.All, emptyOrderBy)); + Assert.ThrowsAsync (async () => await inbox.SortAsync (searchOptions, UniqueIdRange.All, SearchQuery.All, emptyOrderBy)); + + Assert.Throws (() => inbox.Sort ((string) null)); + Assert.ThrowsAsync (async () => await inbox.SortAsync ((string) null)); + Assert.Throws (() => inbox.Sort (string.Empty)); + Assert.ThrowsAsync (async () => await inbox.SortAsync (string.Empty)); + + // Thread + Assert.Throws (() => inbox.Thread ((ThreadingAlgorithm) 500, SearchQuery.All)); + Assert.ThrowsAsync (async () => await inbox.ThreadAsync ((ThreadingAlgorithm) 500, SearchQuery.All)); + Assert.Throws (() => inbox.Thread (ThreadingAlgorithm.References, null)); + Assert.ThrowsAsync (async () => await inbox.ThreadAsync (ThreadingAlgorithm.References, null)); + Assert.Throws (() => inbox.Thread ((IList) null, ThreadingAlgorithm.References, SearchQuery.All)); + Assert.ThrowsAsync (async () => await inbox.ThreadAsync ((IList) null, ThreadingAlgorithm.References, SearchQuery.All)); + Assert.Throws (() => inbox.Thread (UniqueIdRange.All, (ThreadingAlgorithm) 500, SearchQuery.All)); + Assert.ThrowsAsync (async () => await inbox.ThreadAsync (UniqueIdRange.All, (ThreadingAlgorithm) 500, SearchQuery.All)); + Assert.Throws (() => inbox.Thread (UniqueIdRange.All, ThreadingAlgorithm.References, null)); + Assert.ThrowsAsync (async () => await inbox.ThreadAsync (UniqueIdRange.All, ThreadingAlgorithm.References, null)); + + client.Disconnect (false); + } + } + + static IList CreateSearchKeywordsCommands () + { + return new List { + new ImapReplayCommand ("", "dovecot.greeting.txt"), + new ImapReplayCommand ("A00000000 LOGIN username password\r\n", "dovecot.authenticate+filters.txt"), + new ImapReplayCommand ("A00000001 NAMESPACE\r\n", "dovecot.namespace.txt"), + new ImapReplayCommand ("A00000002 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-inbox.txt"), + new ImapReplayCommand ("A00000003 LIST (SPECIAL-USE) \"\" \"*\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-special-use.txt"), + new ImapReplayCommand ("A00000004 SELECT INBOX (CONDSTORE)\r\n", "common.select-inbox.txt"), + new ImapReplayCommand ("A00000005 UID SEARCH RETURN (ALL) KEYWORD \\flag\r\n", "dovecot.search-all.txt"), + new ImapReplayCommand ("A00000006 UID SEARCH RETURN (ALL) UNKEYWORD \\flag\r\n", "dovecot.search-all.txt"), + new ImapReplayCommand ("A00000007 UID SEARCH RETURN (ALL) KEYWORD \"two words\"\r\n", "dovecot.search-all.txt"), + new ImapReplayCommand ("A00000008 UID SEARCH RETURN (ALL) UNKEYWORD \"two words\"\r\n", "dovecot.search-all.txt"), + new ImapReplayCommand ("A00000009 UID SEARCH RETURN (ALL) KEYWORD $IsSpam\r\n", "dovecot.search-all.txt"), + new ImapReplayCommand ("A00000010 UID SEARCH RETURN (ALL) UNKEYWORD $IsSpam\r\n", "dovecot.search-all.txt"), + new ImapReplayCommand ("A00000011 UID SEARCH RETURN (ALL) KEYWORD \\flag KEYWORD \"two words\" KEYWORD $IsSpam\r\n", "dovecot.search-all.txt"), + new ImapReplayCommand ("A00000012 UID SEARCH RETURN (ALL) UNKEYWORD \\flag UNKEYWORD \"two words\" UNKEYWORD $IsSpam\r\n", "dovecot.search-all.txt") + }; + } + + [Test] + public void TestSearchKeywords () + { + var commands = CreateSearchKeywordsCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + var credentials = new NetworkCredential ("username", "password"); + + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + client.Authenticate (credentials); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities.HasFlag (ImapCapabilities.Filters), Is.True, "ImapCapabilities.Filters"); + + var inbox = (ImapFolder) client.Inbox; + inbox.Open (FolderAccess.ReadWrite); + + var uids = inbox.Search (SearchQuery.HasKeyword ("\\flag")); + Assert.That (uids, Has.Count.EqualTo (14), "Unexpected number of UIDs"); + for (int i = 0; i < uids.Count; i++) + Assert.That (uids[i].Id, Is.EqualTo (i + 1), $"Unexpected value for uids[{i}]"); + + uids = inbox.Search (SearchQuery.NotKeyword ("\\flag")); + Assert.That (uids, Has.Count.EqualTo (14), "Unexpected number of UIDs"); + for (int i = 0; i < uids.Count; i++) + Assert.That (uids[i].Id, Is.EqualTo (i + 1), $"Unexpected value for uids[{i}]"); + + uids = inbox.Search (SearchQuery.HasKeyword ("two words")); + Assert.That (uids, Has.Count.EqualTo (14), "Unexpected number of UIDs"); + for (int i = 0; i < uids.Count; i++) + Assert.That (uids[i].Id, Is.EqualTo (i + 1), $"Unexpected value for uids[{i}]"); + + uids = inbox.Search (SearchQuery.NotKeyword ("two words")); + Assert.That (uids, Has.Count.EqualTo (14), "Unexpected number of UIDs"); + for (int i = 0; i < uids.Count; i++) + Assert.That (uids[i].Id, Is.EqualTo (i + 1), $"Unexpected value for uids[{i}]"); + + uids = inbox.Search (SearchQuery.HasKeyword ("$IsSpam")); + Assert.That (uids, Has.Count.EqualTo (14), "Unexpected number of UIDs"); + for (int i = 0; i < uids.Count; i++) + Assert.That (uids[i].Id, Is.EqualTo (i + 1), $"Unexpected value for uids[{i}]"); + + uids = inbox.Search (SearchQuery.NotKeyword ("$IsSpam")); + Assert.That (uids, Has.Count.EqualTo (14), "Unexpected number of UIDs"); + for (int i = 0; i < uids.Count; i++) + Assert.That (uids[i].Id, Is.EqualTo (i + 1), $"Unexpected value for uids[{i}]"); + + uids = inbox.Search (SearchQuery.HasKeywords ("\\flag", "two words", "$IsSpam")); + Assert.That (uids, Has.Count.EqualTo (14), "Unexpected number of UIDs"); + for (int i = 0; i < uids.Count; i++) + Assert.That (uids[i].Id, Is.EqualTo (i + 1), $"Unexpected value for uids[{i}]"); + + uids = inbox.Search (SearchQuery.NotKeywords ("\\flag", "two words", "$IsSpam")); + Assert.That (uids, Has.Count.EqualTo (14), "Unexpected number of UIDs"); + for (int i = 0; i < uids.Count; i++) + Assert.That (uids[i].Id, Is.EqualTo (i + 1), $"Unexpected value for uids[{i}]"); + + client.Disconnect (false); + } + } + + [Test] + public async Task TestSearchKeywordsAsync () + { + var commands = CreateSearchKeywordsCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + var credentials = new NetworkCredential ("username", "password"); + + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + await client.AuthenticateAsync (credentials); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities.HasFlag (ImapCapabilities.Filters), Is.True, "ImapCapabilities.Filters"); + + var inbox = (ImapFolder) client.Inbox; + await inbox.OpenAsync (FolderAccess.ReadWrite); + + var uids = await inbox.SearchAsync (SearchQuery.HasKeyword ("\\flag")); + Assert.That (uids, Has.Count.EqualTo (14), "Unexpected number of UIDs"); + for (int i = 0; i < uids.Count; i++) + Assert.That (uids[i].Id, Is.EqualTo (i + 1), $"Unexpected value for uids[{i}]"); + + uids = await inbox.SearchAsync (SearchQuery.NotKeyword ("\\flag")); + Assert.That (uids, Has.Count.EqualTo (14), "Unexpected number of UIDs"); + for (int i = 0; i < uids.Count; i++) + Assert.That (uids[i].Id, Is.EqualTo (i + 1), $"Unexpected value for uids[{i}]"); + + uids = await inbox.SearchAsync (SearchQuery.HasKeyword ("two words")); + Assert.That (uids, Has.Count.EqualTo (14), "Unexpected number of UIDs"); + for (int i = 0; i < uids.Count; i++) + Assert.That (uids[i].Id, Is.EqualTo (i + 1), $"Unexpected value for uids[{i}]"); + + uids = await inbox.SearchAsync (SearchQuery.NotKeyword ("two words")); + Assert.That (uids, Has.Count.EqualTo (14), "Unexpected number of UIDs"); + for (int i = 0; i < uids.Count; i++) + Assert.That (uids[i].Id, Is.EqualTo (i + 1), $"Unexpected value for uids[{i}]"); + + uids = await inbox.SearchAsync (SearchQuery.HasKeyword ("$IsSpam")); + Assert.That (uids, Has.Count.EqualTo (14), "Unexpected number of UIDs"); + for (int i = 0; i < uids.Count; i++) + Assert.That (uids[i].Id, Is.EqualTo (i + 1), $"Unexpected value for uids[{i}]"); + + uids = await inbox.SearchAsync (SearchQuery.NotKeyword ("$IsSpam")); + Assert.That (uids, Has.Count.EqualTo (14), "Unexpected number of UIDs"); + for (int i = 0; i < uids.Count; i++) + Assert.That (uids[i].Id, Is.EqualTo (i + 1), $"Unexpected value for uids[{i}]"); + + uids = await inbox.SearchAsync (SearchQuery.HasKeywords ("\\flag", "two words", "$IsSpam")); + Assert.That (uids, Has.Count.EqualTo (14), "Unexpected number of UIDs"); + for (int i = 0; i < uids.Count; i++) + Assert.That (uids[i].Id, Is.EqualTo (i + 1), $"Unexpected value for uids[{i}]"); + + uids = await inbox.SearchAsync (SearchQuery.NotKeywords ("\\flag", "two words", "$IsSpam")); + Assert.That (uids, Has.Count.EqualTo (14), "Unexpected number of UIDs"); + for (int i = 0; i < uids.Count; i++) + Assert.That (uids[i].Id, Is.EqualTo (i + 1), $"Unexpected value for uids[{i}]"); + + await client.DisconnectAsync (false); + } + } + + static IList CreateSearchFilterCommands () + { + return new List { + new ImapReplayCommand ("", "dovecot.greeting.txt"), + new ImapReplayCommand ("A00000000 LOGIN username password\r\n", "dovecot.authenticate+filters.txt"), + new ImapReplayCommand ("A00000001 NAMESPACE\r\n", "dovecot.namespace.txt"), + new ImapReplayCommand ("A00000002 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-inbox.txt"), + new ImapReplayCommand ("A00000003 LIST (SPECIAL-USE) \"\" \"*\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-special-use.txt"), + new ImapReplayCommand ("A00000004 SELECT INBOX (CONDSTORE)\r\n", "common.select-inbox.txt"), + new ImapReplayCommand ("A00000005 UID SEARCH RETURN (ALL) FILTER MyFilter\r\n", "dovecot.search-all.txt"), + new ImapReplayCommand ("A00000006 UID SEARCH RETURN (ALL) FILTER MyUndefinedFilter\r\n", Encoding.ASCII.GetBytes ("A00000006 NO [UNDEFINED-FILTER MyUndefinedFilter] THe specified filter is undefined.\r\n")), + }; + } + + [Test] + public void TestSearchFilter () + { + var commands = CreateSearchFilterCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + var credentials = new NetworkCredential ("username", "password"); + + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + client.Authenticate (credentials); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities.HasFlag (ImapCapabilities.Filters), Is.True, "ImapCapabilities.Filters"); + + var inbox = (ImapFolder) client.Inbox; + inbox.Open (FolderAccess.ReadWrite); + + var uids = inbox.Search (SearchQuery.Filter ("MyFilter")); + Assert.That (uids, Has.Count.EqualTo (14), "Unexpected number of UIDs"); + for (int i = 0; i < uids.Count; i++) + Assert.That (uids[i].Id, Is.EqualTo (i + 1), $"Unexpected value for uids[{i}]"); + + Assert.Throws (() => inbox.Search (SearchQuery.Filter ("MyUndefinedFilter"))); + + // Now disable the FILTERS extension and try again... + client.Capabilities &= ~ImapCapabilities.Filters; + Assert.Throws (() => inbox.Search (SearchQuery.Filter ("MyFilter"))); + + client.Disconnect (false); + } + } + + [Test] + public async Task TestSearchFilterAsync () + { + var commands = CreateSearchFilterCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + var credentials = new NetworkCredential ("username", "password"); + + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + await client.AuthenticateAsync (credentials); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities.HasFlag (ImapCapabilities.Filters), Is.True, "ImapCapabilities.Filters"); + + var inbox = (ImapFolder) client.Inbox; + await inbox.OpenAsync (FolderAccess.ReadWrite); + + var uids = await inbox.SearchAsync (SearchQuery.Filter ("MyFilter")); + Assert.That (uids, Has.Count.EqualTo (14), "Unexpected number of UIDs"); + for (int i = 0; i < uids.Count; i++) + Assert.That (uids[i].Id, Is.EqualTo (i + 1), $"Unexpected value for uids[{i}]"); + + Assert.ThrowsAsync (() => inbox.SearchAsync (SearchQuery.Filter ("MyUndefinedFilter"))); + + // Now disable the SAVEDATE extension and try again... + client.Capabilities &= ~ImapCapabilities.Filters; + Assert.ThrowsAsync (() => inbox.SearchAsync (SearchQuery.Filter ("MyFilter"))); + + await client.DisconnectAsync (false); + } + } + + static IList CreateSearchFuzzyCommands () + { + return new List { + new ImapReplayCommand ("", "dovecot.greeting.txt"), + new ImapReplayCommand ("A00000000 LOGIN username password\r\n", "dovecot.authenticate+fuzzy.txt"), + new ImapReplayCommand ("A00000001 NAMESPACE\r\n", "dovecot.namespace.txt"), + new ImapReplayCommand ("A00000002 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-inbox.txt"), + new ImapReplayCommand ("A00000003 LIST (SPECIAL-USE) \"\" \"*\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-special-use.txt"), + new ImapReplayCommand ("A00000004 SELECT INBOX (CONDSTORE)\r\n", "common.select-inbox.txt"), + new ImapReplayCommand ("A00000005 UID SEARCH RETURN (ALL) FUZZY BODY fuzzy-match\r\n", "dovecot.search-all.txt"), + }; + } + + [Test] + public void TestSearchFuzzy () + { + var commands = CreateSearchFuzzyCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + var credentials = new NetworkCredential ("username", "password"); + + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + client.Authenticate (credentials); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities.HasFlag (ImapCapabilities.FuzzySearch), Is.True, "ImapCapabilities.FuzzySearch"); + + var inbox = (ImapFolder) client.Inbox; + inbox.Open (FolderAccess.ReadWrite); + + var uids = inbox.Search (SearchQuery.Fuzzy (SearchQuery.BodyContains ("fuzzy-match"))); + Assert.That (uids, Has.Count.EqualTo (14), "Unexpected number of UIDs"); + for (int i = 0; i < uids.Count; i++) + Assert.That (uids[i].Id, Is.EqualTo (i + 1), $"Unexpected value for uids[{i}]"); + + // Now disable the FUZZY extension and try again... + client.Capabilities &= ~ImapCapabilities.FuzzySearch; + Assert.Throws (() => inbox.Search (SearchQuery.Fuzzy (SearchQuery.BodyContains ("fuzzy-match")))); + + client.Disconnect (false); + } + } + + [Test] + public async Task TestSearchFuzzyAsync () + { + var commands = CreateSearchFuzzyCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + var credentials = new NetworkCredential ("username", "password"); + + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + await client.AuthenticateAsync (credentials); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities.HasFlag (ImapCapabilities.FuzzySearch), Is.True, "ImapCapabilities.FuzzySearch"); + + var inbox = (ImapFolder) client.Inbox; + await inbox.OpenAsync (FolderAccess.ReadWrite); + + var uids = await inbox.SearchAsync (SearchQuery.Fuzzy (SearchQuery.BodyContains ("fuzzy-match"))); + Assert.That (uids, Has.Count.EqualTo (14), "Unexpected number of UIDs"); + for (int i = 0; i < uids.Count; i++) + Assert.That (uids[i].Id, Is.EqualTo (i + 1), $"Unexpected value for uids[{i}]"); + + // Now disable the FUZZY extension and try again... + client.Capabilities &= ~ImapCapabilities.FuzzySearch; + Assert.ThrowsAsync (() => inbox.SearchAsync (SearchQuery.Fuzzy (SearchQuery.BodyContains ("fuzzy-match")))); + + await client.DisconnectAsync (false); + } + } + + static IList CreateSearchSaveDateCommands () + { + return new List { + new ImapReplayCommand ("", "dovecot.greeting.txt"), + new ImapReplayCommand ("A00000000 LOGIN username password\r\n", "dovecot.authenticate+savedate.txt"), + new ImapReplayCommand ("A00000001 NAMESPACE\r\n", "dovecot.namespace.txt"), + new ImapReplayCommand ("A00000002 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-inbox.txt"), + new ImapReplayCommand ("A00000003 LIST (SPECIAL-USE) \"\" \"*\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-special-use.txt"), + new ImapReplayCommand ("A00000004 SELECT INBOX (CONDSTORE)\r\n", "common.select-inbox.txt"), + new ImapReplayCommand ("A00000005 UID SEARCH RETURN (ALL) SAVEDATESUPPORTED\r\n", "dovecot.search-all.txt"), + new ImapReplayCommand ("A00000006 UID SEARCH RETURN (ALL) SAVEDBEFORE 12-Oct-2016\r\n", "dovecot.search-all.txt"), + new ImapReplayCommand ("A00000007 UID SEARCH RETURN (ALL) SAVEDON 12-Oct-2016\r\n", "dovecot.search-all.txt"), + new ImapReplayCommand ("A00000008 UID SEARCH RETURN (ALL) SAVEDSINCE 12-Oct-2016\r\n", "dovecot.search-all.txt"), + }; + } + + [Test] + public void TestSearchSaveDate () + { + var commands = CreateSearchSaveDateCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + var credentials = new NetworkCredential ("username", "password"); + + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + client.Authenticate (credentials); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities.HasFlag (ImapCapabilities.SaveDate), Is.True, "ImapCapabilities.SaveDate"); + + var inbox = (ImapFolder) client.Inbox; + inbox.Open (FolderAccess.ReadWrite); + + var uids = inbox.Search (SearchQuery.SaveDateSupported); + Assert.That (uids, Has.Count.EqualTo (14), "Unexpected number of UIDs"); + for (int i = 0; i < uids.Count; i++) + Assert.That (uids[i].Id, Is.EqualTo (i + 1), $"Unexpected value for uids[{i}]"); + + uids = inbox.Search (SearchQuery.SavedBefore (new DateTime (2016, 10, 12))); + Assert.That (uids, Has.Count.EqualTo (14), "Unexpected number of UIDs"); + for (int i = 0; i < uids.Count; i++) + Assert.That (uids[i].Id, Is.EqualTo (i + 1), $"Unexpected value for uids[{i}]"); + + uids = inbox.Search (SearchQuery.SavedOn (new DateTime (2016, 10, 12))); + Assert.That (uids, Has.Count.EqualTo (14), "Unexpected number of UIDs"); + for (int i = 0; i < uids.Count; i++) + Assert.That (uids[i].Id, Is.EqualTo (i + 1), $"Unexpected value for uids[{i}]"); + + uids = inbox.Search (SearchQuery.SavedSince (new DateTime (2016, 10, 12))); + Assert.That (uids, Has.Count.EqualTo (14), "Unexpected number of UIDs"); + for (int i = 0; i < uids.Count; i++) + Assert.That (uids[i].Id, Is.EqualTo (i + 1), $"Unexpected value for uids[{i}]"); + + // Now disable the SAVEDATE extension and try again... + client.Capabilities &= ~ImapCapabilities.SaveDate; + Assert.Throws (() => inbox.Search (SearchQuery.SaveDateSupported)); + Assert.Throws (() => inbox.Search (SearchQuery.SavedBefore (new DateTime (2016, 10, 12)))); + Assert.Throws (() => inbox.Search (SearchQuery.SavedOn (new DateTime (2016, 10, 12)))); + Assert.Throws (() => inbox.Search (SearchQuery.SavedSince (new DateTime (2016, 10, 12)))); + + client.Disconnect (false); + } + } + + [Test] + public async Task TestSearchSaveDateAsync () + { + var commands = CreateSearchSaveDateCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + var credentials = new NetworkCredential ("username", "password"); + + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + await client.AuthenticateAsync (credentials); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities.HasFlag (ImapCapabilities.SaveDate), Is.True, "ImapCapabilities.SaveDate"); + + var inbox = (ImapFolder) client.Inbox; + await inbox.OpenAsync (FolderAccess.ReadWrite); + + var uids = await inbox.SearchAsync (SearchQuery.SaveDateSupported); + Assert.That (uids, Has.Count.EqualTo (14), "Unexpected number of UIDs"); + for (int i = 0; i < uids.Count; i++) + Assert.That (uids[i].Id, Is.EqualTo (i + 1), $"Unexpected value for uids[{i}]"); + + uids = await inbox.SearchAsync (SearchQuery.SavedBefore (new DateTime (2016, 10, 12))); + Assert.That (uids, Has.Count.EqualTo (14), "Unexpected number of UIDs"); + for (int i = 0; i < uids.Count; i++) + Assert.That (uids[i].Id, Is.EqualTo (i + 1), $"Unexpected value for uids[{i}]"); + + uids = await inbox.SearchAsync (SearchQuery.SavedOn (new DateTime (2016, 10, 12))); + Assert.That (uids, Has.Count.EqualTo (14), "Unexpected number of UIDs"); + for (int i = 0; i < uids.Count; i++) + Assert.That (uids[i].Id, Is.EqualTo (i + 1), $"Unexpected value for uids[{i}]"); + + uids = await inbox.SearchAsync (SearchQuery.SavedSince (new DateTime (2016, 10, 12))); + Assert.That (uids, Has.Count.EqualTo (14), "Unexpected number of UIDs"); + for (int i = 0; i < uids.Count; i++) + Assert.That (uids[i].Id, Is.EqualTo (i + 1), $"Unexpected value for uids[{i}]"); + + // Now disable the SAVEDATE extension and try again... + client.Capabilities &= ~ImapCapabilities.SaveDate; + Assert.ThrowsAsync (() => inbox.SearchAsync (SearchQuery.SaveDateSupported)); + Assert.ThrowsAsync (() => inbox.SearchAsync (SearchQuery.SavedBefore (new DateTime (2016, 10, 12)))); + Assert.ThrowsAsync (() => inbox.SearchAsync (SearchQuery.SavedOn (new DateTime (2016, 10, 12)))); + Assert.ThrowsAsync (() => inbox.SearchAsync (SearchQuery.SavedSince (new DateTime (2016, 10, 12)))); + + await client.DisconnectAsync (false); + } + } + + static List CreateRawUnicodeSearchCommands () + { + return new List { + new ImapReplayCommand ("", "dovecot.greeting.txt"), + new ImapReplayCommand ("A00000000 LOGIN username password\r\n", "dovecot.authenticate+gmail-capabilities.txt"), + new ImapReplayCommand ("A00000001 NAMESPACE\r\n", "dovecot.namespace.txt"), + new ImapReplayCommand ("A00000002 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-inbox.txt"), + new ImapReplayCommand ("A00000003 LIST (SPECIAL-USE) \"\" \"*\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-special-use.txt"), + new ImapReplayCommand ("A00000004 SELECT INBOX (CONDSTORE)\r\n", "common.select-inbox.txt"), + new ImapReplayCommand (Encoding.UTF8, "A00000005 UID SEARCH SUBJECT {13+}\r\nComunicação\r\n", "dovecot.search-raw.txt") + }; + } + + [Test] + public void TestRawUnicodeSearch () + { + var commands = CreateRawUnicodeSearchCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + var credentials = new NetworkCredential ("username", "password"); + + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + client.Authenticate (credentials); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Inbox.SyncRoot, Is.InstanceOf (), "SyncRoot"); + + var inbox = (ImapFolder) client.Inbox; + inbox.Open (FolderAccess.ReadWrite); + + var matches = inbox.Search ("SUBJECT {13+}\r\nComunicação"); + Assert.That (matches.Max.HasValue, Is.True, "MAX should always be set"); + Assert.That (matches.Max.Value.Id, Is.EqualTo (14), "Unexpected MAX value"); + Assert.That (matches.Min.HasValue, Is.True, "MIN should always be set"); + Assert.That (matches.Min.Value.Id, Is.EqualTo (1), "Unexpected MIN value"); + Assert.That (matches.Count, Is.EqualTo (14), "COUNT should always be set"); + Assert.That (matches.UniqueIds, Has.Count.EqualTo (14)); + for (int i = 0; i < matches.UniqueIds.Count; i++) + Assert.That (matches.UniqueIds[i].Id, Is.EqualTo (i + 1)); + + client.Disconnect (false); + } + } + + [Test] + public async Task TestRawUnicodeSearchAsync () + { + var commands = CreateRawUnicodeSearchCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + var credentials = new NetworkCredential ("username", "password"); + + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + await client.AuthenticateAsync (credentials); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Inbox.SyncRoot, Is.InstanceOf (), "SyncRoot"); + + var inbox = (ImapFolder) client.Inbox; + await inbox.OpenAsync (FolderAccess.ReadWrite); + + var matches = await inbox.SearchAsync ("SUBJECT {13+}\r\nComunicação"); + Assert.That (matches.Max.HasValue, Is.True, "MAX should always be set"); + Assert.That (matches.Max.Value.Id, Is.EqualTo (14), "Unexpected MAX value"); + Assert.That (matches.Min.HasValue, Is.True, "MIN should always be set"); + Assert.That (matches.Min.Value.Id, Is.EqualTo (1), "Unexpected MIN value"); + Assert.That (matches.Count, Is.EqualTo (14), "COUNT should always be set"); + Assert.That (matches.UniqueIds, Has.Count.EqualTo (14)); + for (int i = 0; i < matches.UniqueIds.Count; i++) + Assert.That (matches.UniqueIds[i].Id, Is.EqualTo (i + 1)); + + await client.DisconnectAsync (false); + } + } + + static List CreateSearchStringWithSpacesCommands () + { + return new List { + new ImapReplayCommand ("", "yahoo.greeting.txt"), + new ImapReplayCommand ("A00000000 LOGIN username password\r\n", ImapReplayCommandResponse.OK), + new ImapReplayCommand ("A00000001 CAPABILITY\r\n", "yahoo.capabilities.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "yahoo.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\"\r\n", "yahoo.list-inbox.txt"), + new ImapReplayCommand ("A00000004 EXAMINE Inbox\r\n", "yahoo.examine-inbox.txt"), + new ImapReplayCommand (Encoding.UTF8, "A00000005 UID SEARCH SUBJECT \"Yahoo Mail\"\r\n", "yahoo.search.txt") + }; + } + + [Test] + public void TestSearchStringWithSpaces () + { + var commands = CreateSearchStringWithSpacesCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + var credentials = new NetworkCredential ("username", "password"); + + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + client.Authenticate (credentials); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Inbox.SyncRoot, Is.InstanceOf (), "SyncRoot"); + + var inbox = (ImapFolder) client.Inbox; + inbox.Open (FolderAccess.ReadOnly); + + var uids = inbox.Search (SearchQuery.SubjectContains ("Yahoo Mail")); + Assert.That (uids, Has.Count.EqualTo (14)); + for (int i = 0; i < uids.Count; i++) + Assert.That (uids[i].Id, Is.EqualTo (i + 1)); + + client.Disconnect (false); + } + } + + [Test] + public async Task TestSearchStringWithSpacesAsync () + { + var commands = CreateSearchStringWithSpacesCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + var credentials = new NetworkCredential ("username", "password"); + + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + await client.AuthenticateAsync (credentials); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Inbox.SyncRoot, Is.InstanceOf (), "SyncRoot"); + + var inbox = (ImapFolder) client.Inbox; + await inbox.OpenAsync (FolderAccess.ReadOnly); + + var uids = await inbox.SearchAsync (SearchQuery.SubjectContains ("Yahoo Mail")); + Assert.That (uids, Has.Count.EqualTo (14)); + for (int i = 0; i < uids.Count; i++) + Assert.That (uids[i].Id, Is.EqualTo (i + 1)); + + await client.DisconnectAsync (false); + } + } + + static List CreateSearchBadCharsetFallbackCommands () + { + var badCharsetResponse = Encoding.ASCII.GetBytes ("A00000005 NO [BADCHARSET (US-ASCII)] The specified charset is not supported.\r\n"); + + return new List { + new ImapReplayCommand ("", "dovecot.greeting.txt"), + new ImapReplayCommand ("A00000000 LOGIN username password\r\n", "dovecot.authenticate+gmail-capabilities.txt"), + new ImapReplayCommand ("A00000001 NAMESPACE\r\n", "dovecot.namespace.txt"), + new ImapReplayCommand ("A00000002 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-inbox.txt"), + new ImapReplayCommand ("A00000003 LIST (SPECIAL-USE) \"\" \"*\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-special-use.txt"), + new ImapReplayCommand ("A00000004 SELECT INBOX (CONDSTORE)\r\n", "common.select-inbox.txt"), + new ImapReplayCommand (Encoding.UTF8, "A00000005 UID SEARCH RETURN (ALL) CHARSET UTF-8 SUBJECT {12+}\r\nпривет\r\n", badCharsetResponse), + new ImapReplayCommand ("A00000006 UID SEARCH RETURN (ALL) SUBJECT {6+}\r\n?@825B\r\n", "dovecot.search-raw.txt") + }; + } + + [Test] + public void TestSearchBadCharsetFallback () + { + var commands = CreateSearchBadCharsetFallbackCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + var credentials = new NetworkCredential ("username", "password"); + + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + client.Authenticate (credentials); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Inbox.SyncRoot, Is.InstanceOf (), "SyncRoot"); + + var inbox = (ImapFolder) client.Inbox; + inbox.Open (FolderAccess.ReadWrite); + + var uids = inbox.Search (SearchQuery.SubjectContains ("привет")); + Assert.That (uids, Has.Count.EqualTo (14)); + for (int i = 0; i < uids.Count; i++) + Assert.That (uids[i].Id, Is.EqualTo (i + 1)); + + client.Disconnect (false); + } + } + + [Test] + public async Task TestSearchBadCharsetFallbackAsync () + { + var commands = CreateSearchBadCharsetFallbackCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + var credentials = new NetworkCredential ("username", "password"); + + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + await client.AuthenticateAsync (credentials); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Inbox.SyncRoot, Is.InstanceOf (), "SyncRoot"); + + var inbox = (ImapFolder) client.Inbox; + await inbox.OpenAsync (FolderAccess.ReadWrite); + + var uids = await inbox.SearchAsync (SearchQuery.SubjectContains ("привет")); + Assert.That (uids, Has.Count.EqualTo (14)); + for (int i = 0; i < uids.Count; i++) + Assert.That (uids[i].Id, Is.EqualTo (i + 1)); + + await client.DisconnectAsync (false); + } + } + + static List CreateSearchWithOptionsBadCharsetFallbackCommands () + { + var badCharsetResponse = Encoding.ASCII.GetBytes ("A00000005 NO [BADCHARSET (US-ASCII)] The specified charset is not supported.\r\n"); + + return new List { + new ImapReplayCommand ("", "dovecot.greeting.txt"), + new ImapReplayCommand ("A00000000 LOGIN username password\r\n", "dovecot.authenticate+gmail-capabilities.txt"), + new ImapReplayCommand ("A00000001 NAMESPACE\r\n", "dovecot.namespace.txt"), + new ImapReplayCommand ("A00000002 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-inbox.txt"), + new ImapReplayCommand ("A00000003 LIST (SPECIAL-USE) \"\" \"*\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-special-use.txt"), + new ImapReplayCommand ("A00000004 SELECT INBOX (CONDSTORE)\r\n", "common.select-inbox.txt"), + new ImapReplayCommand (Encoding.UTF8, "A00000005 UID SEARCH RETURN (ALL RELEVANCY COUNT MIN MAX) CHARSET UTF-8 SUBJECT {12+}\r\nпривет\r\n", badCharsetResponse), + new ImapReplayCommand ("A00000006 UID SEARCH RETURN (ALL RELEVANCY COUNT MIN MAX) SUBJECT {6+}\r\n?@825B\r\n", "dovecot.search-uids-options.txt") + }; + } + + [Test] + public void TestSearchWithOptionsBadCharsetFallback () + { + var commands = CreateSearchWithOptionsBadCharsetFallbackCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + var credentials = new NetworkCredential ("username", "password"); + + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + client.Authenticate (credentials); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Inbox.SyncRoot, Is.InstanceOf (), "SyncRoot"); + + var inbox = (ImapFolder) client.Inbox; + inbox.Open (FolderAccess.ReadWrite); + + var searchOptions = SearchOptions.All | SearchOptions.Count | SearchOptions.Min | SearchOptions.Max | SearchOptions.Relevancy; + var matches = inbox.Search (searchOptions, SearchQuery.SubjectContains ("привет")); + var expectedMatchedUids = new uint[] { 2, 3, 4, 5, 6, 9, 10, 11, 12, 13 }; + Assert.That (matches.Count, Is.EqualTo (10), "Unexpected COUNT"); + Assert.That (matches.Max.Value.Id, Is.EqualTo (13), "Unexpected MAX"); + Assert.That (matches.Min.Value.Id, Is.EqualTo (2), "Unexpected MIN"); + Assert.That (matches.UniqueIds, Has.Count.EqualTo (10), "Unexpected number of UIDs"); + for (int i = 0; i < matches.UniqueIds.Count; i++) + Assert.That (matches.UniqueIds[i].Id, Is.EqualTo (expectedMatchedUids[i])); + Assert.That (matches.Relevancy, Has.Count.EqualTo (matches.Count), "Unexpected number of relevancy scores"); + + client.Disconnect (false); + } + } + + [Test] + public async Task TestSearchWithOptionsBadCharsetFallbackAsync () + { + var commands = CreateSearchWithOptionsBadCharsetFallbackCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + var credentials = new NetworkCredential ("username", "password"); + + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + await client.AuthenticateAsync (credentials); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Inbox.SyncRoot, Is.InstanceOf (), "SyncRoot"); + + var inbox = (ImapFolder) client.Inbox; + await inbox.OpenAsync (FolderAccess.ReadWrite); + + var searchOptions = SearchOptions.All | SearchOptions.Count | SearchOptions.Min | SearchOptions.Max | SearchOptions.Relevancy; + var matches = await inbox.SearchAsync (searchOptions, SearchQuery.SubjectContains ("привет")); + var expectedMatchedUids = new uint[] { 2, 3, 4, 5, 6, 9, 10, 11, 12, 13 }; + Assert.That (matches.Count, Is.EqualTo (10), "Unexpected COUNT"); + Assert.That (matches.Max.Value.Id, Is.EqualTo (13), "Unexpected MAX"); + Assert.That (matches.Min.Value.Id, Is.EqualTo (2), "Unexpected MIN"); + Assert.That (matches.UniqueIds, Has.Count.EqualTo (10), "Unexpected number of UIDs"); + for (int i = 0; i < matches.UniqueIds.Count; i++) + Assert.That (matches.UniqueIds[i].Id, Is.EqualTo (expectedMatchedUids[i])); + Assert.That (matches.Relevancy, Has.Count.EqualTo (matches.Count), "Unexpected number of relevancy scores"); + + await client.DisconnectAsync (false); + } + } + + static List CreateSortBadCharsetFallbackCommands () + { + var badCharsetResponse = Encoding.ASCII.GetBytes ("A00000005 NO [BADCHARSET (US-ASCII)] The specified charset is not supported.\r\n"); + + return new List { + new ImapReplayCommand ("", "dovecot.greeting.txt"), + new ImapReplayCommand ("A00000000 LOGIN username password\r\n", "dovecot.authenticate+gmail-capabilities.txt"), + new ImapReplayCommand ("A00000001 NAMESPACE\r\n", "dovecot.namespace.txt"), + new ImapReplayCommand ("A00000002 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-inbox.txt"), + new ImapReplayCommand ("A00000003 LIST (SPECIAL-USE) \"\" \"*\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-special-use.txt"), + new ImapReplayCommand ("A00000004 SELECT INBOX (CONDSTORE)\r\n", "common.select-inbox.txt"), + new ImapReplayCommand (Encoding.UTF8, "A00000005 UID SORT RETURN (ALL) (SUBJECT) UTF-8 SUBJECT {12+}\r\nпривет\r\n", badCharsetResponse), + new ImapReplayCommand ("A00000006 UID SORT RETURN (ALL) (SUBJECT) US-ASCII SUBJECT {6+}\r\n?@825B\r\n", "dovecot.sort-raw.txt") + }; + } + + [Test] + public void TestSortBadCharsetFallback () + { + var commands = CreateSortBadCharsetFallbackCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + var credentials = new NetworkCredential ("username", "password"); + + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + client.Authenticate (credentials); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Inbox.SyncRoot, Is.InstanceOf (), "SyncRoot"); + + var inbox = (ImapFolder) client.Inbox; + inbox.Open (FolderAccess.ReadWrite); + + var uids = inbox.Sort (SearchQuery.SubjectContains ("привет"), new OrderBy[] { OrderBy.Subject }); + var expected = new uint[] { 7, 14, 6, 13, 5, 12, 4, 11, 3, 10, 2, 9, 1, 8 }; + for (int i = 0; i < uids.Count; i++) + Assert.That (uids[i].Id, Is.EqualTo (expected[i]), $"Unexpected value for UniqueId[{i}]"); + + client.Disconnect (false); + } + } + + [Test] + public async Task TestSortBadCharsetFallbackAsync () + { + var commands = CreateSortBadCharsetFallbackCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + var credentials = new NetworkCredential ("username", "password"); + + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + await client.AuthenticateAsync (credentials); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Inbox.SyncRoot, Is.InstanceOf (), "SyncRoot"); + + var inbox = (ImapFolder) client.Inbox; + await inbox.OpenAsync (FolderAccess.ReadWrite); + + var uids = await inbox.SortAsync (SearchQuery.SubjectContains ("привет"), new OrderBy[] { OrderBy.Subject }); + var expected = new uint[] { 7, 14, 6, 13, 5, 12, 4, 11, 3, 10, 2, 9, 1, 8 }; + for (int i = 0; i < uids.Count; i++) + Assert.That (uids[i].Id, Is.EqualTo (expected[i]), $"Unexpected value for UniqueId[{i}]"); + + await client.DisconnectAsync (false); + } + } + + static List CreateSortWithOptionsBadCharsetFallbackCommands () + { + var badCharsetResponse = Encoding.ASCII.GetBytes ("A00000005 NO [BADCHARSET (US-ASCII)] The specified charset is not supported.\r\n"); + + return new List { + new ImapReplayCommand ("", "dovecot.greeting.txt"), + new ImapReplayCommand ("A00000000 LOGIN username password\r\n", "dovecot.authenticate+gmail-capabilities.txt"), + new ImapReplayCommand ("A00000001 NAMESPACE\r\n", "dovecot.namespace.txt"), + new ImapReplayCommand ("A00000002 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-inbox.txt"), + new ImapReplayCommand ("A00000003 LIST (SPECIAL-USE) \"\" \"*\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-special-use.txt"), + new ImapReplayCommand ("A00000004 SELECT INBOX (CONDSTORE)\r\n", "common.select-inbox.txt"), + new ImapReplayCommand (Encoding.UTF8, "A00000005 UID SORT RETURN (ALL RELEVANCY COUNT MIN MAX) (ARRIVAL) UTF-8 SUBJECT {12+}\r\nпривет\r\n", badCharsetResponse), + new ImapReplayCommand ("A00000006 UID SORT RETURN (ALL RELEVANCY COUNT MIN MAX) (ARRIVAL) US-ASCII SUBJECT {6+}\r\n?@825B\r\n", "dovecot.sort-uids-options.txt") + }; + } + + [Test] + public void TestSortWithOptionsBadCharsetFallback () + { + var commands = CreateSortWithOptionsBadCharsetFallbackCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + var credentials = new NetworkCredential ("username", "password"); + + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + client.Authenticate (credentials); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Inbox.SyncRoot, Is.InstanceOf (), "SyncRoot"); + + var inbox = (ImapFolder) client.Inbox; + inbox.Open (FolderAccess.ReadWrite); + + var searchOptions = SearchOptions.All | SearchOptions.Count | SearchOptions.Min | SearchOptions.Max | SearchOptions.Relevancy; + var sorted = inbox.Sort (searchOptions, SearchQuery.SubjectContains ("привет"), new OrderBy[] { OrderBy.Arrival }); + Assert.That (sorted.UniqueIds, Has.Count.EqualTo (14), "Unexpected number of UIDs"); + Assert.That (sorted.Relevancy, Has.Count.EqualTo (sorted.Count), "Unexpected number of relevancy scores"); + for (int i = 0; i < sorted.UniqueIds.Count; i++) + Assert.That (sorted.UniqueIds[i].Id, Is.EqualTo (i + 1), $"Unexpected value for UniqueId[{i}]"); + Assert.That (sorted.ModSeq.HasValue, Is.False, "Expected the ModSeq property to be null"); + Assert.That (sorted.Min.Value.Id, Is.EqualTo (1), "Unexpected Min"); + Assert.That (sorted.Max.Value.Id, Is.EqualTo (14), "Unexpected Max"); + Assert.That (sorted.Count, Is.EqualTo (14), "Unexpected Count"); + + client.Disconnect (false); + } + } + + [Test] + public async Task TestSortWithOptionsBadCharsetFallbackAsync () + { + var commands = CreateSortWithOptionsBadCharsetFallbackCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + var credentials = new NetworkCredential ("username", "password"); + + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + await client.AuthenticateAsync (credentials); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Inbox.SyncRoot, Is.InstanceOf (), "SyncRoot"); + + var inbox = (ImapFolder) client.Inbox; + await inbox.OpenAsync (FolderAccess.ReadWrite); + + var searchOptions = SearchOptions.All | SearchOptions.Count | SearchOptions.Min | SearchOptions.Max | SearchOptions.Relevancy; + var sorted = await inbox.SortAsync (searchOptions, SearchQuery.SubjectContains ("привет"), new OrderBy[] { OrderBy.Arrival }); + Assert.That (sorted.UniqueIds, Has.Count.EqualTo (14), "Unexpected number of UIDs"); + Assert.That (sorted.Relevancy, Has.Count.EqualTo (sorted.Count), "Unexpected number of relevancy scores"); + for (int i = 0; i < sorted.UniqueIds.Count; i++) + Assert.That (sorted.UniqueIds[i].Id, Is.EqualTo (i + 1), $"Unexpected value for UniqueId[{i}]"); + Assert.That (sorted.ModSeq.HasValue, Is.False, "Expected the ModSeq property to be null"); + Assert.That (sorted.Min.Value.Id, Is.EqualTo (1), "Unexpected Min"); + Assert.That (sorted.Max.Value.Id, Is.EqualTo (14), "Unexpected Max"); + Assert.That (sorted.Count, Is.EqualTo (14), "Unexpected Count"); + + await client.DisconnectAsync (false); + } + } + + static List CreateThreadBadCharsetFallbackCommands () + { + var badCharsetResponse = Encoding.ASCII.GetBytes ("A00000005 NO [BADCHARSET (US-ASCII)] The specified charset is not supported.\r\n"); + + return new List { + new ImapReplayCommand ("", "dovecot.greeting.txt"), + new ImapReplayCommand ("A00000000 LOGIN username password\r\n", "dovecot.authenticate+gmail-capabilities.txt"), + new ImapReplayCommand ("A00000001 NAMESPACE\r\n", "dovecot.namespace.txt"), + new ImapReplayCommand ("A00000002 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-inbox.txt"), + new ImapReplayCommand ("A00000003 LIST (SPECIAL-USE) \"\" \"*\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-special-use.txt"), + new ImapReplayCommand ("A00000004 SELECT INBOX (CONDSTORE)\r\n", "common.select-inbox.txt"), + //new ImapReplayCommand ("A00000005 UID THREAD REFERENCES US-ASCII \r\n", "dovecot.thread-references.txt"), + //(new ImapReplayCommand ("A00000017 UID THREAD ORDEREDSUBJECT US-ASCII UID 1:* ALL\r\n", "dovecot.thread-orderedsubject.txt"), + new ImapReplayCommand (Encoding.UTF8, "A00000005 UID THREAD REFERENCES UTF-8 SUBJECT {12+}\r\nпривет\r\n", badCharsetResponse), + new ImapReplayCommand ("A00000006 UID THREAD REFERENCES US-ASCII SUBJECT {6+}\r\n?@825B\r\n", "dovecot.thread-references.txt") + }; + } + + [Test] + public void TestThreadBadCharsetFallback () + { + var commands = CreateThreadBadCharsetFallbackCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + var credentials = new NetworkCredential ("username", "password"); + + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + client.Authenticate (credentials); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Inbox.SyncRoot, Is.InstanceOf (), "SyncRoot"); + + var inbox = (ImapFolder) client.Inbox; + inbox.Open (FolderAccess.ReadWrite); + + Assert.That (inbox.Supports (FolderFeature.Threading), Is.True, "Supports threading"); + Assert.That (inbox.ThreadingAlgorithms, Does.Contain (ThreadingAlgorithm.References), "Supports threading by References"); + + var threaded = inbox.Thread (ThreadingAlgorithm.References, SearchQuery.SubjectContains ("привет")); + Assert.That (threaded, Has.Count.EqualTo (2), "Unexpected number of root nodes in threaded results"); + + client.Disconnect (false); + } + } + + [Test] + public async Task TestThreadBadCharsetFallbackAsync () + { + var commands = CreateThreadBadCharsetFallbackCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + var credentials = new NetworkCredential ("username", "password"); + + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + await client.AuthenticateAsync (credentials); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Inbox.SyncRoot, Is.InstanceOf (), "SyncRoot"); + + var inbox = (ImapFolder) client.Inbox; + await inbox.OpenAsync (FolderAccess.ReadWrite); + + Assert.That (inbox.Supports (FolderFeature.Threading), Is.True, "Supports threading"); + Assert.That (inbox.ThreadingAlgorithms, Does.Contain (ThreadingAlgorithm.References), "Supports threading by References"); + + var threaded = await inbox.ThreadAsync (ThreadingAlgorithm.References, SearchQuery.SubjectContains ("привет")); + Assert.That (threaded, Has.Count.EqualTo (2), "Unexpected number of root nodes in threaded results"); + + await client.DisconnectAsync (false); + } + } + + static List CreateThreadUidsBadCharsetFallbackCommands () + { + var badCharsetResponse = Encoding.ASCII.GetBytes ("A00000005 NO [BADCHARSET (US-ASCII)] The specified charset is not supported.\r\n"); + + return new List { + new ImapReplayCommand ("", "dovecot.greeting.txt"), + new ImapReplayCommand ("A00000000 LOGIN username password\r\n", "dovecot.authenticate+gmail-capabilities.txt"), + new ImapReplayCommand ("A00000001 NAMESPACE\r\n", "dovecot.namespace.txt"), + new ImapReplayCommand ("A00000002 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-inbox.txt"), + new ImapReplayCommand ("A00000003 LIST (SPECIAL-USE) \"\" \"*\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-special-use.txt"), + new ImapReplayCommand ("A00000004 SELECT INBOX (CONDSTORE)\r\n", "common.select-inbox.txt"), + new ImapReplayCommand (Encoding.UTF8, "A00000005 UID THREAD REFERENCES UTF-8 UID 1:* SUBJECT {12+}\r\nпривет\r\n", badCharsetResponse), + new ImapReplayCommand ("A00000006 UID THREAD REFERENCES US-ASCII UID 1:* SUBJECT {6+}\r\n?@825B\r\n", "dovecot.thread-references.txt") + }; + } + + [Test] + public void TestThreadUidsBadCharsetFallback () + { + var commands = CreateThreadUidsBadCharsetFallbackCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + var credentials = new NetworkCredential ("username", "password"); + + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + client.Authenticate (credentials); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Inbox.SyncRoot, Is.InstanceOf (), "SyncRoot"); + + var inbox = (ImapFolder) client.Inbox; + inbox.Open (FolderAccess.ReadWrite); + + Assert.That (inbox.Supports (FolderFeature.Threading), Is.True, "Supports threading"); + Assert.That (inbox.ThreadingAlgorithms, Does.Contain (ThreadingAlgorithm.References), "Supports threading by References"); + + var threaded = inbox.Thread (UniqueIdRange.All, ThreadingAlgorithm.References, SearchQuery.SubjectContains ("привет")); + Assert.That (threaded, Has.Count.EqualTo (2), "Unexpected number of root nodes in threaded results"); + + client.Disconnect (false); + } + } + + [Test] + public async Task TestThreadUidsBadCharsetFallbackAsync () + { + var commands = CreateThreadUidsBadCharsetFallbackCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + var credentials = new NetworkCredential ("username", "password"); + + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + await client.AuthenticateAsync (credentials); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Inbox.SyncRoot, Is.InstanceOf (), "SyncRoot"); + + var inbox = (ImapFolder) client.Inbox; + await inbox.OpenAsync (FolderAccess.ReadWrite); + + Assert.That (inbox.Supports (FolderFeature.Threading), Is.True, "Supports threading"); + Assert.That (inbox.ThreadingAlgorithms, Does.Contain (ThreadingAlgorithm.References), "Supports threading by References"); + + var threaded = await inbox.ThreadAsync (UniqueIdRange.All, ThreadingAlgorithm.References, SearchQuery.SubjectContains ("привет")); + Assert.That (threaded, Has.Count.EqualTo (2), "Unexpected number of root nodes in threaded results"); + + await client.DisconnectAsync (false); + } + } + } +} diff --git a/UnitTests/Net/Imap/ImapFolderTests.cs b/UnitTests/Net/Imap/ImapFolderTests.cs new file mode 100644 index 0000000000..1582661778 --- /dev/null +++ b/UnitTests/Net/Imap/ImapFolderTests.cs @@ -0,0 +1,3121 @@ +// +// ImapFolderTests.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System.Net; +using System.Text; +using System.Globalization; + +using MimeKit; + +using MailKit; +using MailKit.Search; +using MailKit.Security; +using MailKit.Net.Imap; + +namespace UnitTests.Net.Imap { + [TestFixture] + public class ImapFolderTests + { + static readonly Encoding Latin1 = Encoding.GetEncoding (28591); + + static MimeMessage CreateThreadableMessage (string subject, string msgid, string references, DateTimeOffset date) + { + var message = new MimeMessage (); + message.From.Add (new MailboxAddress ("Unit Tests", "unit-tests@mimekit.net")); + message.To.Add (new MailboxAddress ("Unit Tests", "unit-tests@mimekit.net")); + message.MessageId = msgid; + message.Subject = subject; + message.Date = date; + + if (references != null) { + foreach (var reference in references.Split (' ')) + message.References.Add (reference); + } + + message.Body = new TextPart ("plain") { Text = "This is the message body.\r\n" }; + + return message; + } + + static Stream GetResourceStream (string name) + { + return typeof (ImapFolderTests).Assembly.GetManifestResourceStream ("UnitTests.Net.Imap.Resources." + name); + } + + [Test] + public void TestArgumentExceptions () + { + var commands = new List { + new ImapReplayCommand ("", "dovecot.greeting.txt"), + new ImapReplayCommand ("A00000000 LOGIN username password\r\n", "dovecot.authenticate+gmail-capabilities.txt"), + new ImapReplayCommand ("A00000001 NAMESPACE\r\n", "dovecot.namespace.txt"), + new ImapReplayCommand ("A00000002 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-inbox.txt"), + new ImapReplayCommand ("A00000003 LIST (SPECIAL-USE) \"\" \"*\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-special-use.txt"), + new ImapReplayCommand ("A00000004 SELECT INBOX (CONDSTORE)\r\n", "common.select-inbox.txt") + }; + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + var credentials = new NetworkCredential ("username", "password"); + + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + client.Authenticate (credentials); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + var personal = client.GetFolder (client.PersonalNamespaces[0]); + var multiappend = new List (); + var dates = new List (); + var messages = new List (); + var flags = new List (); + var now = DateTimeOffset.Now; + var uid = new UniqueId (1); + ReplaceRequest replace = null; + + messages.Add (CreateThreadableMessage ("A", "", null, now.AddMinutes (-7))); + messages.Add (CreateThreadableMessage ("B", "", "", now.AddMinutes (-6))); + messages.Add (CreateThreadableMessage ("C", "", " ", now.AddMinutes (-5))); + messages.Add (CreateThreadableMessage ("D", "", "", now.AddMinutes (-4))); + messages.Add (CreateThreadableMessage ("E", "", " ", now.AddMinutes (-3))); + messages.Add (CreateThreadableMessage ("F", "", "", now.AddMinutes (-2))); + messages.Add (CreateThreadableMessage ("G", "", null, now.AddMinutes (-1))); + messages.Add (CreateThreadableMessage ("H", "", null, now)); + + for (int i = 0; i < messages.Count; i++) { + dates.Add (DateTimeOffset.Now); + flags.Add (MessageFlags.Seen); + multiappend.Add (new AppendRequest (messages[i], flags[i], dates[i])); + replace ??= new ReplaceRequest (messages[i], flags[i], dates[i]); + } + + Assert.That (client.Inbox.SyncRoot, Is.InstanceOf (), "SyncRoot"); + + var inbox = (ImapFolder) client.Inbox; + inbox.Open (FolderAccess.ReadWrite); + + // ImapFolder .ctor + Assert.Throws (() => new ImapFolder (null)); + + // Open + Assert.Throws (() => inbox.Open ((FolderAccess) 500)); + Assert.Throws (() => inbox.Open ((FolderAccess) 500, 0, 0, UniqueIdRange.All)); + Assert.Throws (() => inbox.Open (FolderAccess.ReadOnly, 0, 0, null)); + Assert.ThrowsAsync (() => inbox.OpenAsync ((FolderAccess) 500)); + Assert.ThrowsAsync (() => inbox.OpenAsync ((FolderAccess) 500, 0, 0, UniqueIdRange.All)); + Assert.ThrowsAsync (() => inbox.OpenAsync (FolderAccess.ReadOnly, 0, 0, null)); + + // Create + Assert.Throws (() => inbox.Create (null, true)); + Assert.Throws (() => inbox.Create (string.Empty, true)); + Assert.Throws (() => inbox.Create ("Folder./Name", true)); + Assert.Throws (() => inbox.Create (null, SpecialFolder.All)); + Assert.Throws (() => inbox.Create (string.Empty, SpecialFolder.All)); + Assert.Throws (() => inbox.Create ("Folder./Name", SpecialFolder.All)); + Assert.Throws (() => inbox.Create (null, new SpecialFolder[] { SpecialFolder.All })); + Assert.Throws (() => inbox.Create (string.Empty, new SpecialFolder[] { SpecialFolder.All })); + Assert.Throws (() => inbox.Create ("Folder./Name", new SpecialFolder[] { SpecialFolder.All })); + Assert.Throws (() => inbox.Create ("ValidName", null)); + Assert.Throws (() => inbox.Create ("ValidName", SpecialFolder.All)); + Assert.ThrowsAsync (() => inbox.CreateAsync (null, true)); + Assert.ThrowsAsync (() => inbox.CreateAsync (string.Empty, true)); + Assert.ThrowsAsync (() => inbox.CreateAsync ("Folder./Name", true)); + Assert.ThrowsAsync (() => inbox.CreateAsync (null, SpecialFolder.All)); + Assert.ThrowsAsync (() => inbox.CreateAsync (string.Empty, SpecialFolder.All)); + Assert.ThrowsAsync (() => inbox.CreateAsync ("Folder./Name", SpecialFolder.All)); + Assert.ThrowsAsync (() => inbox.CreateAsync (null, new SpecialFolder[] { SpecialFolder.All })); + Assert.ThrowsAsync (() => inbox.CreateAsync (string.Empty, new SpecialFolder[] { SpecialFolder.All })); + Assert.ThrowsAsync (() => inbox.CreateAsync ("Folder./Name", new SpecialFolder[] { SpecialFolder.All })); + Assert.ThrowsAsync (() => inbox.CreateAsync ("ValidName", null)); + Assert.ThrowsAsync (() => inbox.CreateAsync ("ValidName", SpecialFolder.All)); + + // Rename + Assert.Throws (() => inbox.Rename (null, "NewName")); + Assert.Throws (() => inbox.Rename (personal, null)); + Assert.Throws (() => inbox.Rename (personal, string.Empty)); + Assert.ThrowsAsync (() => inbox.RenameAsync (null, "NewName")); + Assert.ThrowsAsync (() => inbox.RenameAsync (personal, null)); + Assert.ThrowsAsync (() => inbox.RenameAsync (personal, string.Empty)); + + // GetSubfolder + Assert.Throws (() => inbox.GetSubfolder (null)); + Assert.Throws (() => inbox.GetSubfolder (string.Empty)); + Assert.ThrowsAsync (() => inbox.GetSubfolderAsync (null)); + Assert.ThrowsAsync (() => inbox.GetSubfolderAsync (string.Empty)); + + // GetMetadata + Assert.Throws (() => client.GetMetadata (null, new MetadataTag[] { MetadataTag.PrivateComment })); + Assert.Throws (() => client.GetMetadata (new MetadataOptions (), null)); + Assert.ThrowsAsync (() => client.GetMetadataAsync (null, new MetadataTag[] { MetadataTag.PrivateComment })); + Assert.ThrowsAsync (() => client.GetMetadataAsync (new MetadataOptions (), null)); + Assert.Throws (() => inbox.GetMetadata (null, new MetadataTag[] { MetadataTag.PrivateComment })); + Assert.Throws (() => inbox.GetMetadata (new MetadataOptions (), null)); + Assert.ThrowsAsync (() => inbox.GetMetadataAsync (null, new MetadataTag[] { MetadataTag.PrivateComment })); + Assert.ThrowsAsync (() => inbox.GetMetadataAsync (new MetadataOptions (), null)); + + // SetMetadata + Assert.Throws (() => client.SetMetadata (null)); + Assert.ThrowsAsync (() => client.SetMetadataAsync (null)); + Assert.Throws (() => inbox.SetMetadata (null)); + Assert.ThrowsAsync (() => inbox.SetMetadataAsync (null)); + + // Expunge + Assert.Throws (() => inbox.Expunge (null)); + Assert.ThrowsAsync (() => inbox.ExpungeAsync (null)); + + // Append + Assert.Throws (() => inbox.Append ((MimeMessage) null)); + Assert.ThrowsAsync (() => inbox.AppendAsync ((MimeMessage) null)); + Assert.Throws (() => inbox.Append (null, messages[0])); + Assert.ThrowsAsync (() => inbox.AppendAsync (null, messages[0])); + Assert.Throws (() => inbox.Append (FormatOptions.Default, (MimeMessage) null)); + Assert.ThrowsAsync (() => inbox.AppendAsync (FormatOptions.Default, (MimeMessage) null)); + Assert.Throws (() => inbox.Append (null, MessageFlags.None, DateTimeOffset.Now)); + Assert.ThrowsAsync (() => inbox.AppendAsync (null, MessageFlags.None, DateTimeOffset.Now)); + Assert.Throws (() => inbox.Append (null, messages[0], MessageFlags.None, DateTimeOffset.Now)); + Assert.ThrowsAsync (() => inbox.AppendAsync (null, messages[0], MessageFlags.None, DateTimeOffset.Now)); + Assert.Throws (() => inbox.Append (FormatOptions.Default, null, MessageFlags.None, DateTimeOffset.Now)); + Assert.ThrowsAsync (() => inbox.AppendAsync (FormatOptions.Default, null, MessageFlags.None, DateTimeOffset.Now)); + Assert.Throws (() => inbox.Append ((IAppendRequest) null)); + Assert.ThrowsAsync (() => inbox.AppendAsync ((IAppendRequest) null)); + Assert.Throws (() => inbox.Append (null, new AppendRequest (messages[0]))); + Assert.ThrowsAsync (() => inbox.AppendAsync (null, new AppendRequest (messages[0]))); + Assert.Throws (() => inbox.Append (FormatOptions.Default, (IAppendRequest) null)); + Assert.ThrowsAsync (() => inbox.AppendAsync (FormatOptions.Default, (IAppendRequest) null)); + + // MultiAppend + Assert.Throws (() => inbox.Append (null, flags)); + Assert.ThrowsAsync (() => inbox.AppendAsync (null, flags)); + Assert.Throws (() => inbox.Append (new MimeMessage[] { null }, flags)); + Assert.ThrowsAsync (() => inbox.AppendAsync (new MimeMessage[] { null }, flags)); + Assert.Throws (() => inbox.Append (messages, null)); + Assert.ThrowsAsync (() => inbox.AppendAsync (messages, null)); + Assert.Throws (() => inbox.Append (messages, new MessageFlags[messages.Count - 1])); + Assert.ThrowsAsync (() => inbox.AppendAsync (messages, new MessageFlags[messages.Count - 1])); + Assert.Throws (() => inbox.Append (null, messages, flags)); + Assert.ThrowsAsync (() => inbox.AppendAsync (null, messages, flags)); + Assert.Throws (() => inbox.Append (FormatOptions.Default, null, flags)); + Assert.ThrowsAsync (() => inbox.AppendAsync (FormatOptions.Default, null, flags)); + Assert.Throws (() => inbox.Append (FormatOptions.Default, new MimeMessage[] { null }, flags)); + Assert.ThrowsAsync (() => inbox.AppendAsync (FormatOptions.Default, new MimeMessage[] { null }, flags)); + Assert.Throws (() => inbox.Append (FormatOptions.Default, messages, null)); + Assert.ThrowsAsync (() => inbox.AppendAsync (FormatOptions.Default, messages, null)); + Assert.Throws (() => inbox.Append (FormatOptions.Default, messages, new MessageFlags[messages.Count - 1])); + Assert.ThrowsAsync (() => inbox.AppendAsync (FormatOptions.Default, messages, new MessageFlags[messages.Count - 1])); + Assert.Throws (() => inbox.Append (null, flags, dates)); + Assert.ThrowsAsync (() => inbox.AppendAsync (null, flags, dates)); + Assert.Throws (() => inbox.Append (new MimeMessage[] { null }, flags, dates)); + Assert.ThrowsAsync (() => inbox.AppendAsync (new MimeMessage[] { null }, flags, dates)); + Assert.Throws (() => inbox.Append (messages, null, dates)); + Assert.ThrowsAsync (() => inbox.AppendAsync (messages, null, dates)); + Assert.Throws (() => inbox.Append (messages, flags, null)); + Assert.ThrowsAsync (() => inbox.AppendAsync (messages, flags, null)); + Assert.Throws (() => inbox.Append (messages, new MessageFlags[messages.Count - 1], dates)); + Assert.ThrowsAsync (() => inbox.AppendAsync (messages, new MessageFlags[messages.Count - 1], dates)); + Assert.Throws (() => inbox.Append (messages, flags, new DateTimeOffset[messages.Count - 1])); + Assert.ThrowsAsync (() => inbox.AppendAsync (messages, flags, new DateTimeOffset[messages.Count - 1])); + Assert.Throws (() => inbox.Append (null, messages, flags, dates)); + Assert.ThrowsAsync (() => inbox.AppendAsync (null, messages, flags, dates)); + Assert.Throws (() => inbox.Append (FormatOptions.Default, null, flags, dates)); + Assert.ThrowsAsync (() => inbox.AppendAsync (FormatOptions.Default, null, flags, dates)); + Assert.Throws (() => inbox.Append (FormatOptions.Default, new MimeMessage[] { null }, flags, dates)); + Assert.ThrowsAsync (() => inbox.AppendAsync (FormatOptions.Default, new MimeMessage[] { null }, flags, dates)); + Assert.Throws (() => inbox.Append (FormatOptions.Default, messages, null, dates)); + Assert.ThrowsAsync (() => inbox.AppendAsync (FormatOptions.Default, messages, null, dates)); + Assert.Throws (() => inbox.Append (FormatOptions.Default, messages, flags, null)); + Assert.ThrowsAsync (() => inbox.AppendAsync (FormatOptions.Default, messages, flags, null)); + Assert.Throws (() => inbox.Append (FormatOptions.Default, messages, new MessageFlags[messages.Count - 1], dates)); + Assert.ThrowsAsync (() => inbox.AppendAsync (FormatOptions.Default, messages, new MessageFlags[messages.Count - 1], dates)); + Assert.Throws (() => inbox.Append (FormatOptions.Default, messages, flags, new DateTimeOffset[messages.Count - 1])); + Assert.ThrowsAsync (() => inbox.AppendAsync (FormatOptions.Default, messages, flags, new DateTimeOffset[messages.Count - 1])); + Assert.Throws (() => inbox.Append ((IList) null)); + Assert.ThrowsAsync (() => inbox.AppendAsync ((IList) null)); + Assert.Throws (() => inbox.Append (null, multiappend)); + Assert.ThrowsAsync (() => inbox.AppendAsync (null, multiappend)); + Assert.Throws (() => inbox.Append (new IAppendRequest[1])); + Assert.ThrowsAsync (() => inbox.AppendAsync (new IAppendRequest[1])); + Assert.Throws (() => inbox.Append (FormatOptions.Default, new IAppendRequest[1])); + Assert.ThrowsAsync (() => inbox.AppendAsync (FormatOptions.Default, new IAppendRequest[1])); + + // Replace + Assert.Throws (() => inbox.Replace (UniqueId.Invalid, messages[0])); + Assert.ThrowsAsync (() => inbox.ReplaceAsync (UniqueId.Invalid, messages[0])); + Assert.Throws (() => inbox.Replace (UniqueId.Invalid, messages[0], MessageFlags.None, DateTimeOffset.Now)); + Assert.ThrowsAsync (() => inbox.ReplaceAsync (UniqueId.Invalid, messages[0], MessageFlags.None, DateTimeOffset.Now)); + Assert.Throws (() => inbox.Replace (uid, null)); + Assert.ThrowsAsync (() => inbox.ReplaceAsync (uid, null)); + Assert.Throws (() => inbox.Replace (uid, null, MessageFlags.None, DateTimeOffset.Now)); + Assert.ThrowsAsync (() => inbox.ReplaceAsync (uid, null, MessageFlags.None, DateTimeOffset.Now)); + Assert.Throws (() => inbox.Replace (null, uid, messages[0])); + Assert.ThrowsAsync (() => inbox.ReplaceAsync (null, uid, messages[0])); + Assert.Throws (() => inbox.Replace (null, uid, messages[0], MessageFlags.None, DateTimeOffset.Now)); + Assert.ThrowsAsync (() => inbox.ReplaceAsync (null, uid, messages[0], MessageFlags.None, DateTimeOffset.Now)); + Assert.Throws (() => inbox.Replace (-1, messages[0])); + Assert.ThrowsAsync (() => inbox.ReplaceAsync (-1, messages[0])); + Assert.Throws (() => inbox.Replace (-1, messages[0], MessageFlags.None, DateTimeOffset.Now)); + Assert.ThrowsAsync (() => inbox.ReplaceAsync (-1, messages[0], MessageFlags.None, DateTimeOffset.Now)); + Assert.Throws (() => inbox.Replace (0, null)); + Assert.ThrowsAsync (() => inbox.ReplaceAsync (0, null)); + Assert.Throws (() => inbox.Replace (0, null, MessageFlags.None, DateTimeOffset.Now)); + Assert.ThrowsAsync (() => inbox.ReplaceAsync (0, null, MessageFlags.None, DateTimeOffset.Now)); + Assert.Throws (() => inbox.Replace (null, 0, messages[0])); + Assert.ThrowsAsync (() => inbox.ReplaceAsync (null, 0, messages[0])); + Assert.Throws (() => inbox.Replace (null, 0, messages[0], MessageFlags.None, DateTimeOffset.Now)); + Assert.ThrowsAsync (() => inbox.ReplaceAsync (null, 0, messages[0], MessageFlags.None, DateTimeOffset.Now)); + Assert.Throws (() => inbox.Replace (UniqueId.Invalid, replace)); + Assert.ThrowsAsync (() => inbox.ReplaceAsync (UniqueId.Invalid, replace)); + Assert.Throws (() => inbox.Replace (UniqueId.MinValue, null)); + Assert.ThrowsAsync (() => inbox.ReplaceAsync (UniqueId.MinValue, null)); + Assert.Throws (() => inbox.Replace (null, UniqueId.MinValue, replace)); + Assert.ThrowsAsync (() => inbox.ReplaceAsync (null, UniqueId.MinValue, replace)); + Assert.Throws (() => inbox.Replace (FormatOptions.Default, UniqueId.Invalid, replace)); + Assert.ThrowsAsync (() => inbox.ReplaceAsync (FormatOptions.Default, UniqueId.Invalid, replace)); + Assert.Throws (() => inbox.Replace (FormatOptions.Default, UniqueId.MinValue, null)); + Assert.ThrowsAsync (() => inbox.ReplaceAsync (FormatOptions.Default, UniqueId.MinValue, null)); + Assert.Throws (() => inbox.Replace (-1, replace)); + Assert.ThrowsAsync (() => inbox.ReplaceAsync (-1, replace)); + Assert.Throws (() => inbox.Replace (0, null)); + Assert.ThrowsAsync (() => inbox.ReplaceAsync (0, null)); + Assert.Throws (() => inbox.Replace (null, 0, replace)); + Assert.ThrowsAsync (() => inbox.ReplaceAsync (null, 0, replace)); + Assert.Throws (() => inbox.Replace (FormatOptions.Default, -1, replace)); + Assert.ThrowsAsync (() => inbox.ReplaceAsync (FormatOptions.Default, -1, replace)); + Assert.Throws (() => inbox.Replace (FormatOptions.Default, 0, null)); + Assert.ThrowsAsync (() => inbox.ReplaceAsync (FormatOptions.Default, 0, null)); + + // CopyTo + Assert.Throws (() => inbox.CopyTo (UniqueId.Invalid, inbox)); + Assert.ThrowsAsync (() => inbox.CopyToAsync (UniqueId.Invalid, inbox)); + Assert.Throws (() => inbox.CopyTo (UniqueId.MinValue, null)); + Assert.ThrowsAsync (() => inbox.CopyToAsync (UniqueId.MinValue, null)); + Assert.Throws (() => inbox.CopyTo ((IList) null, inbox)); + Assert.ThrowsAsync (() => inbox.CopyToAsync ((IList) null, inbox)); + Assert.Throws (() => inbox.CopyTo (UniqueIdRange.All, null)); + Assert.ThrowsAsync (() => inbox.CopyToAsync (UniqueIdRange.All, null)); + Assert.Throws (() => inbox.CopyTo (-1, inbox)); + Assert.ThrowsAsync (() => inbox.CopyToAsync (-1, inbox)); + Assert.Throws (() => inbox.CopyTo (0, null)); + Assert.ThrowsAsync (() => inbox.CopyToAsync (0, null)); + Assert.Throws (() => inbox.CopyTo ((IList) null, inbox)); + Assert.ThrowsAsync (() => inbox.CopyToAsync ((IList) null, inbox)); + Assert.Throws (() => inbox.CopyTo (new int[] { 0 }, null)); + Assert.ThrowsAsync (() => inbox.CopyToAsync (new int[] { 0 }, null)); + + // MoveTo + Assert.Throws (() => inbox.MoveTo (UniqueId.Invalid, inbox)); + Assert.ThrowsAsync (() => inbox.MoveToAsync (UniqueId.Invalid, inbox)); + Assert.Throws (() => inbox.MoveTo (UniqueId.MinValue, null)); + Assert.ThrowsAsync (() => inbox.MoveToAsync (UniqueId.MinValue, null)); + Assert.Throws (() => inbox.MoveTo ((IList) null, inbox)); + Assert.ThrowsAsync (() => inbox.MoveToAsync ((IList) null, inbox)); + Assert.Throws (() => inbox.MoveTo (UniqueIdRange.All, null)); + Assert.ThrowsAsync (() => inbox.MoveToAsync (UniqueIdRange.All, null)); + Assert.Throws (() => inbox.MoveTo (-1, inbox)); + Assert.ThrowsAsync (() => inbox.MoveToAsync (-1, inbox)); + Assert.Throws (() => inbox.MoveTo (0, null)); + Assert.ThrowsAsync (() => inbox.MoveToAsync (0, null)); + Assert.Throws (() => inbox.MoveTo ((IList) null, inbox)); + Assert.ThrowsAsync (() => inbox.MoveToAsync ((IList) null, inbox)); + Assert.Throws (() => inbox.MoveTo (new int[] { 0 }, null)); + Assert.ThrowsAsync (() => inbox.MoveToAsync (new int[] { 0 }, null)); + + client.Disconnect (false); + + foreach (var message in messages) + message.Dispose (); + } + } + + [Test] + public void TestNotSupportedExceptions () + { + var commands = new List { + new ImapReplayCommand ("", "dovecot.greeting.txt"), + new ImapReplayCommand ("A00000000 LOGIN username password\r\n", "dovecot.authenticate+gmail-capabilities.txt"), + new ImapReplayCommand ("A00000001 NAMESPACE\r\n", "dovecot.namespace.txt"), + new ImapReplayCommand ("A00000002 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-inbox.txt"), + new ImapReplayCommand ("A00000003 LIST (SPECIAL-USE) \"\" \"*\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-special-use.txt"), + //new ImapReplayCommand ("A00000004 SELECT INBOX\r\n", "common.select-inbox.txt") + }; + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + var credentials = new NetworkCredential ("username", "password"); + + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + client.Authenticate (credentials); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + // disable all features + client.Capabilities = ImapCapabilities.None; + + var personal = client.GetFolder (client.PersonalNamespaces[0]); + var dates = new List (); + var messages = new List (); + var flags = new List (); + var now = DateTimeOffset.Now; + + messages.Add (CreateThreadableMessage ("A", "", null, now.AddMinutes (-7))); + messages.Add (CreateThreadableMessage ("B", "", "", now.AddMinutes (-6))); + messages.Add (CreateThreadableMessage ("C", "", " ", now.AddMinutes (-5))); + messages.Add (CreateThreadableMessage ("D", "", "", now.AddMinutes (-4))); + messages.Add (CreateThreadableMessage ("E", "", " ", now.AddMinutes (-3))); + messages.Add (CreateThreadableMessage ("F", "", "", now.AddMinutes (-2))); + messages.Add (CreateThreadableMessage ("G", "", null, now.AddMinutes (-1))); + messages.Add (CreateThreadableMessage ("H", "", null, now)); + + for (int i = 0; i < messages.Count; i++) { + dates.Add (DateTimeOffset.Now); + flags.Add (MessageFlags.Seen); + } + + Assert.That (client.Inbox.SyncRoot, Is.InstanceOf (), "SyncRoot"); + + var inbox = (ImapFolder) client.Inbox; + + // Open + Assert.Throws (() => inbox.Open (FolderAccess.ReadOnly, 0, 0, UniqueIdRange.All)); + Assert.ThrowsAsync (() => inbox.OpenAsync (FolderAccess.ReadOnly, 0, 0, UniqueIdRange.All)); + + // Create + Assert.Throws (() => inbox.Create ("Folder", SpecialFolder.All)); + Assert.ThrowsAsync (() => inbox.CreateAsync ("Folder", SpecialFolder.All)); + + // Rename - TODO + + // Append + var international = FormatOptions.Default.Clone (); + international.International = true; + Assert.Throws (() => inbox.Append (international, messages[0])); + Assert.ThrowsAsync (() => inbox.AppendAsync (international, messages[0])); + Assert.Throws (() => inbox.Append (international, messages[0], flags[0])); + Assert.ThrowsAsync (() => inbox.AppendAsync (international, messages[0], flags[0])); + Assert.Throws (() => inbox.Append (international, messages[0], flags[0], dates[0])); + Assert.ThrowsAsync (() => inbox.AppendAsync (international, messages[0], flags[0], dates[0])); + + // MultiAppend + //Assert.Throws (() => inbox.Append (international, messages)); + //Assert.ThrowsAsync (() => inbox.AppendAsync (international, messages)); + Assert.Throws (() => inbox.Append (international, messages, flags)); + Assert.ThrowsAsync (() => inbox.AppendAsync (international, messages, flags)); + Assert.Throws (() => inbox.Append (international, messages, flags, dates)); + Assert.ThrowsAsync (() => inbox.AppendAsync (international, messages, flags, dates)); + + // Status + Assert.Throws (() => inbox.Status (StatusItems.Count)); + Assert.ThrowsAsync (() => inbox.StatusAsync (StatusItems.Count)); + + // GetAccessControlList + Assert.Throws (() => inbox.GetAccessControlList ()); + Assert.ThrowsAsync (() => inbox.GetAccessControlListAsync ()); + + // GetAccessRights + Assert.Throws (() => inbox.GetAccessRights ("name")); + Assert.ThrowsAsync (() => inbox.GetAccessRightsAsync ("name")); + + // GetMyAccessRights + Assert.Throws (() => inbox.GetMyAccessRights ()); + Assert.ThrowsAsync (() => inbox.GetMyAccessRightsAsync ()); + + // RemoveAccess + Assert.Throws (() => inbox.RemoveAccess ("name")); + Assert.ThrowsAsync (() => inbox.RemoveAccessAsync ("name")); + + // GetMetadata + Assert.Throws (() => client.GetMetadata (MetadataTag.PrivateComment)); + Assert.ThrowsAsync (() => client.GetMetadataAsync (MetadataTag.PrivateComment)); + Assert.Throws (() => inbox.GetMetadata (MetadataTag.PrivateComment)); + Assert.ThrowsAsync (() => inbox.GetMetadataAsync (MetadataTag.PrivateComment)); + Assert.Throws (() => client.GetMetadata (new MetadataOptions (), new MetadataTag[] { MetadataTag.PrivateComment })); + Assert.ThrowsAsync (() => client.GetMetadataAsync (new MetadataOptions (), new MetadataTag[] { MetadataTag.PrivateComment })); + Assert.Throws (() => inbox.GetMetadata (new MetadataOptions (), new MetadataTag[] { MetadataTag.PrivateComment })); + Assert.ThrowsAsync (() => inbox.GetMetadataAsync (new MetadataOptions (), new MetadataTag[] { MetadataTag.PrivateComment })); + + // SetMetadata + Assert.Throws (() => client.SetMetadata (new MetadataCollection ())); + Assert.ThrowsAsync (() => client.SetMetadataAsync (new MetadataCollection ())); + Assert.Throws (() => inbox.SetMetadata (new MetadataCollection ())); + Assert.ThrowsAsync (() => inbox.SetMetadataAsync (new MetadataCollection ())); + + // GetQuota + Assert.Throws (() => inbox.GetQuota ()); + Assert.ThrowsAsync (() => inbox.GetQuotaAsync ()); + + // SetQuota + Assert.Throws (() => inbox.SetQuota (5, 10)); + Assert.ThrowsAsync (() => inbox.SetQuotaAsync (5, 10)); + + client.Disconnect (false); + + foreach (var message in messages) + message.Dispose (); + } + } + + static IList CreateLiteralFolderNamesCommands () + { + return new List { + new ImapReplayCommand ("", "common.basic-greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "common.capability.txt"), + new ImapReplayCommand ("A00000001 LOGIN username password\r\n", ImapReplayCommandResponse.OK), + new ImapReplayCommand ("A00000002 CAPABILITY\r\n", "common.capability.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"\"\r\n", "common.list-namespace.txt"), + new ImapReplayCommand ("A00000004 LIST \"\" \"INBOX\"\r\n", "common.list-inbox.txt"), + new ImapReplayCommand ("A00000005 LIST \"\" \"%\"\r\n", "common.list-literal-subfolders.txt"), + new ImapReplayCommand ("A00000006 STATUS \"Literal Folder Name\" (MESSAGES)\r\n", "common.status-literal-folder.txt"), + }; + } + + [Test] + public void TestLiteralFolderNames () + { + var commands = CreateLiteralFolderNamesCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + var personal = client.GetFolder (client.PersonalNamespaces[0]); + var subfolders = personal.GetSubfolders (false); + + Assert.That (subfolders, Has.Count.EqualTo (2), "Count"); + Assert.That (subfolders[0].Name, Is.EqualTo ("INBOX")); + Assert.That (subfolders[1].Name, Is.EqualTo ("Literal Folder Name")); + + subfolders[1].Status (StatusItems.Count); + + Assert.That (subfolders[1], Has.Count.EqualTo (60), "Count"); + + client.Disconnect (false); + } + } + + [Test] + public async Task TestLiteralFolderNamesAsync () + { + var commands = CreateLiteralFolderNamesCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + var personal = client.GetFolder (client.PersonalNamespaces[0]); + var subfolders = await personal.GetSubfoldersAsync (false); + + Assert.That (subfolders, Has.Count.EqualTo (2), "Count"); + Assert.That (subfolders[0].Name, Is.EqualTo ("INBOX")); + Assert.That (subfolders[1].Name, Is.EqualTo ("Literal Folder Name")); + + await subfolders[1].StatusAsync (StatusItems.Count); + + Assert.That (subfolders[1], Has.Count.EqualTo (60), "Count"); + + await client.DisconnectAsync (false); + } + } + + static IList CreateNilDirectorySeparatorCommands () + { + return new List { + new ImapReplayCommand ("", "common.basic-greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "common.capability.txt"), + new ImapReplayCommand ("A00000001 LOGIN username password\r\n", ImapReplayCommandResponse.OK), + new ImapReplayCommand ("A00000002 CAPABILITY\r\n", "common.capability.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"\"\r\n", "common.list-namespace.txt"), + new ImapReplayCommand ("A00000004 LIST \"\" \"INBOX\"\r\n", "common.list-inbox.txt"), + new ImapReplayCommand ("A00000005 LIST \"\" \"%\"\r\n", "common.list-nil-folder-delim.txt"), + }; + } + + [Test] + public void TestNilDirectorySeparator () + { + var commands = CreateNilDirectorySeparatorCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + var personal = client.GetFolder (client.PersonalNamespaces[0]); + var subfolders = personal.GetSubfolders (false); + + Assert.That (subfolders, Has.Count.EqualTo (3), "Count"); + Assert.That (subfolders[0].Name, Is.EqualTo ("INBOX")); + Assert.That (subfolders[1].Name, Is.EqualTo ("Folder1")); + Assert.That (subfolders[1].DirectorySeparator, Is.EqualTo ('\0')); + Assert.That (subfolders[2].Name, Is.EqualTo ("Folder2")); + Assert.That (subfolders[2].DirectorySeparator, Is.EqualTo ('\0')); + + Assert.Throws (() => subfolders[1].GetSubfolder ("Subfolder")); + + var empty = subfolders[1].GetSubfolders (false); + Assert.That (empty, Is.Empty, "GetSubfolders when DirectorySeparator is nil"); + + client.Disconnect (false); + } + } + + [Test] + public async Task TestNilDirectorySeparatorAsync () + { + var commands = CreateNilDirectorySeparatorCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + var personal = client.GetFolder (client.PersonalNamespaces[0]); + var subfolders = await personal.GetSubfoldersAsync (false); + + Assert.That (subfolders, Has.Count.EqualTo (3), "Count"); + Assert.That (subfolders[0].Name, Is.EqualTo ("INBOX")); + Assert.That (subfolders[1].Name, Is.EqualTo ("Folder1")); + Assert.That (subfolders[1].DirectorySeparator, Is.EqualTo ('\0')); + Assert.That (subfolders[2].Name, Is.EqualTo ("Folder2")); + Assert.That (subfolders[2].DirectorySeparator, Is.EqualTo ('\0')); + + Assert.ThrowsAsync (() => subfolders[1].GetSubfolderAsync ("Subfolder")); + + var empty = await subfolders[1].GetSubfoldersAsync (false); + Assert.That (empty, Is.Empty, "GetSubfolders when DirectorySeparator is nil"); + + await client.DisconnectAsync (false); + } + } + + static IList CreateAppendLimitCommands () + { + return new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt"), + new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "gmail.authenticate-no-appendlimit-value.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-inbox.txt"), + new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt"), + new ImapReplayCommand ("A00000005 STATUS INBOX (APPENDLIMIT)\r\n", "gmail.status-inbox-appendlimit.txt"), + new ImapReplayCommand ("A00000006 STATUS INBOX (APPENDLIMIT)\r\n", "gmail.status-inbox-appendlimit-nil.txt"), + new ImapReplayCommand ("A00000007 LIST \"\" \"%\" RETURN (SUBSCRIBED CHILDREN STATUS (MESSAGES UNSEEN APPENDLIMIT SIZE))\r\n", "gmail.list-personal-status-appendlimit.txt") + }; + } + + [Test] + public void TestAppendLimit () + { + var commands = CreateAppendLimitCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities.HasFlag (ImapCapabilities.AppendLimit), Is.True, "ImapCapabilities.AppendLimit"); + Assert.That (client.AppendLimit, Is.Null, "AppendLimit"); + + client.Inbox.Status (StatusItems.AppendLimit); + Assert.That (client.Inbox.AppendLimit, Is.EqualTo (35651584), "Inbox.AppendLimit"); + + client.Inbox.Status (StatusItems.AppendLimit); + Assert.That (client.Inbox.AppendLimit, Is.Null, "Inbox.AppendLimit NIL"); + + var personal = client.GetFolder (client.PersonalNamespaces[0]); + var subfolders = personal.GetSubfolders (StatusItems.Count | StatusItems.Unread | StatusItems.Size | StatusItems.AppendLimit, subscribedOnly: false); + Assert.That (subfolders, Has.Count.EqualTo (2), "Count"); + Assert.That (subfolders[0].Name, Is.EqualTo ("INBOX")); + Assert.That (subfolders[0].AppendLimit, Is.EqualTo (1234567890), "Inbox.AppendLimit"); + Assert.That (subfolders[0], Has.Count.EqualTo (10), "Inbox.Count"); + Assert.That (subfolders[0].Unread, Is.EqualTo (1), "Inbox.Unread"); + Assert.That (subfolders[0].Size, Is.EqualTo (123456789), "Inbox.Size"); + + client.Disconnect (false); + } + } + + [Test] + public async Task TestAppendLimitAsync () + { + var commands = CreateAppendLimitCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities.HasFlag (ImapCapabilities.AppendLimit), Is.True, "ImapCapabilities.AppendLimit"); + Assert.That (client.AppendLimit, Is.Null, "AppendLimit"); + + await client.Inbox.StatusAsync (StatusItems.AppendLimit); + Assert.That (client.Inbox.AppendLimit, Is.EqualTo (35651584), "Inbox.AppendLimit"); + + await client.Inbox.StatusAsync (StatusItems.AppendLimit); + Assert.That (client.Inbox.AppendLimit, Is.Null, "Inbox.AppendLimit NIL"); + + var personal = client.GetFolder (client.PersonalNamespaces[0]); + var subfolders = await personal.GetSubfoldersAsync (StatusItems.Count | StatusItems.Unread | StatusItems.Size | StatusItems.AppendLimit, subscribedOnly: false); + Assert.That (subfolders, Has.Count.EqualTo (2), "Count"); + Assert.That (subfolders[0].Name, Is.EqualTo ("INBOX")); + Assert.That (subfolders[0].AppendLimit, Is.EqualTo (1234567890), "Inbox.AppendLimit"); + Assert.That (subfolders[0], Has.Count.EqualTo (10), "Inbox.Count"); + Assert.That (subfolders[0].Unread, Is.EqualTo (1), "Inbox.Unread"); + Assert.That (subfolders[0].Size, Is.EqualTo (123456789), "Inbox.Size"); + + await client.DisconnectAsync (false); + } + } + + static List CreateAppendCommands (bool withKeywords, bool withInternalDates, out List messages, out List flags, out List> keywords, out List internalDates) + { + var commands = new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt"), + new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "gmail.authenticate.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-inbox.txt"), + new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt") + }; + + internalDates = withInternalDates ? new List () : null; + keywords = withKeywords ? new List> () : null; + messages = new List (); + flags = new List (); + var command = new StringBuilder (); + int id = 5; + + for (int i = 0; i < 8; i++) { + MimeMessage message; + string latin1; + long length; + + using (var resource = GetResourceStream (string.Format ("common.message.{0}.msg", i))) + message = MimeMessage.Load (resource); + + messages.Add (message); + flags.Add (MessageFlags.Seen); + + if (withKeywords) + keywords.Add (new List { "$NotJunk" }); + + if (withInternalDates) + internalDates.Add (message.Date); + + using (var stream = new MemoryStream ()) { + var options = FormatOptions.Default.Clone (); + options.NewLineFormat = NewLineFormat.Dos; + options.EnsureNewLine = true; + + message.WriteTo (options, stream); + length = stream.Length; + stream.Position = 0; + + using (var reader = new StreamReader (stream, Latin1)) + latin1 = reader.ReadToEnd (); + } + + var tag = string.Format ("A{0:D8}", id++); + command.Clear (); + + if (withKeywords) + command.AppendFormat ("{0} APPEND INBOX (\\Seen $NotJunk) ", tag); + else + command.AppendFormat ("{0} APPEND INBOX (\\Seen) ", tag); + + if (withInternalDates) + command.AppendFormat ("\"{0}\" ", ImapUtils.FormatInternalDate (message.Date)); + + if (length > 4096) { + command.Append ('{').Append (length.ToString (CultureInfo.InvariantCulture)).Append ("}\r\n"); + commands.Add (new ImapReplayCommand (command.ToString (), ImapReplayCommandResponse.Plus)); + commands.Add (new ImapReplayCommand (tag, latin1 + "\r\n", string.Format ("dovecot.append.{0}.txt", i + 1))); + } else { + command.Append ('{').Append (length.ToString (CultureInfo.InvariantCulture)).Append ("+}\r\n").Append (latin1).Append ("\r\n"); + commands.Add (new ImapReplayCommand (command.ToString (), string.Format ("dovecot.append.{0}.txt", i + 1))); + } + } + + commands.Add (new ImapReplayCommand (string.Format ("A{0:D8} LOGOUT\r\n", id), "gmail.logout.txt")); + + return commands; + } + + [TestCase (false, false, TestName = "TestAppend")] + [TestCase (true, false, TestName = "TestAppendWithKeywords")] + [TestCase (false, true, TestName = "TestAppendWithInternalDates")] + [TestCase (true, true, TestName = "TestAppendWithKeywordsAndInternalDates")] + public void TestAppend (bool withKeywords, bool withInternalDates) + { + var commands = CreateAppendCommands (withKeywords, withInternalDates, out var messages, out var flags, out var keywords, out var internalDates); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + for (int i = 0; i < messages.Count; i++) { + UniqueId? uid; + + if (withKeywords) { + AppendRequest request; + + if (withInternalDates) { + request = new AppendRequest (messages[i], flags[i], keywords[i], internalDates[i]); + } else { + request = new AppendRequest (messages[i], flags[i], keywords[i]); + } + + uid = client.Inbox.Append (request); + } else if (withInternalDates) { + uid = client.Inbox.Append (messages[i], flags[i], internalDates[i]); + } else { + uid = client.Inbox.Append (messages[i], flags[i]); + } + + Assert.That (uid.HasValue, Is.True, "Expected a UIDAPPEND resp-code"); + Assert.That (uid.Value.Id, Is.EqualTo (i + 1), "Unexpected UID"); + + messages[i].Dispose (); + } + + client.Disconnect (true); + } + } + + [TestCase (false, false, TestName = "TestAppendAsync")] + [TestCase (true, false, TestName = "TestAppendWithKeywordsAsync")] + [TestCase (false, true, TestName = "TestAppendWithInternalDatesAsync")] + [TestCase (true, true, TestName = "TestAppendWithKeywordsAndInternalDatesAsync")] + public async Task TestAppendAsync (bool withKeywords, bool withInternalDates) + { + var commands = CreateAppendCommands (withKeywords, withInternalDates, out var messages, out var flags, out var keywords, out var internalDates); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + for (int i = 0; i < messages.Count; i++) { + UniqueId? uid; + + if (withKeywords) { + AppendRequest request; + + if (withInternalDates) { + request = new AppendRequest (messages[i], flags[i], keywords[i], internalDates[i]); + } else { + request = new AppendRequest (messages[i], flags[i], keywords[i]); + } + + uid = await client.Inbox.AppendAsync (request); + } else if (withInternalDates) { + uid = await client.Inbox.AppendAsync (messages[i], flags[i], internalDates[i]); + } else { + uid = await client.Inbox.AppendAsync (messages[i], flags[i]); + } + + Assert.That (uid.HasValue, Is.True, "Expected a UIDAPPEND resp-code"); + Assert.That (uid.Value.Id, Is.EqualTo (i + 1), "Unexpected UID"); + + messages[i].Dispose (); + } + + await client.DisconnectAsync (true); + } + } + + static List CreateMultiAppendCommands (bool withKeywords, bool withInternalDates, out List messages, out List flags, out List> keywords, out List internalDates) + { + var commands = new List { + new ImapReplayCommand ("", "dovecot.greeting.txt"), + new ImapReplayCommand ("A00000000 LOGIN username password\r\n", "dovecot.authenticate.txt"), + new ImapReplayCommand ("A00000001 NAMESPACE\r\n", "dovecot.namespace.txt"), + new ImapReplayCommand ("A00000002 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-inbox.txt"), + new ImapReplayCommand ("A00000003 LIST (SPECIAL-USE) \"\" \"*\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-special-use.txt") + }; + + var command = new StringBuilder ("A00000004 APPEND INBOX"); + var now = DateTimeOffset.Now; + + internalDates = withInternalDates ? new List () : null; + keywords = withKeywords ? new List> () : null; + messages = new List (); + flags = new List (); + + messages.Add (CreateThreadableMessage ("A", "", null, now.AddMinutes (-7))); + messages.Add (CreateThreadableMessage ("B", "", "", now.AddMinutes (-6))); + messages.Add (CreateThreadableMessage ("C", "", " ", now.AddMinutes (-5))); + messages.Add (CreateThreadableMessage ("D", "", "", now.AddMinutes (-4))); + messages.Add (CreateThreadableMessage ("E", "", " ", now.AddMinutes (-3))); + messages.Add (CreateThreadableMessage ("F", "", "", now.AddMinutes (-2))); + messages.Add (CreateThreadableMessage ("G", "", null, now.AddMinutes (-1))); + messages.Add (CreateThreadableMessage ("H", "", null, now)); + + for (int i = 0; i < messages.Count; i++) { + var message = messages[i]; + string latin1; + long length; + + flags.Add (MessageFlags.Seen); + + if (withKeywords) + keywords.Add (new List { "$NotJunk" }); + + if (withInternalDates) + internalDates.Add (messages[i].Date); + + using (var stream = new MemoryStream ()) { + var options = FormatOptions.Default.Clone (); + options.NewLineFormat = NewLineFormat.Dos; + + message.WriteTo (options, stream); + length = stream.Length; + stream.Position = 0; + + using (var reader = new StreamReader (stream, Latin1)) + latin1 = reader.ReadToEnd (); + } + + if (withKeywords) + command.Append (" (\\Seen $NotJunk) "); + else + command.Append (" (\\Seen) "); + + if (withInternalDates) + command.AppendFormat ("\"{0}\" ", ImapUtils.FormatInternalDate (message.Date)); + + command.Append ('{'); + command.AppendFormat ("{0}+", length); + command.Append ("}\r\n"); + command.Append (latin1); + } + command.Append ("\r\n"); + commands.Add (new ImapReplayCommand (command.ToString (), "dovecot.multiappend.txt")); + + for (int i = 0; i < messages.Count; i++) { + var message = messages[i]; + string latin1; + long length; + + command.Clear (); + command.AppendFormat ("A{0:D8} APPEND INBOX", i + 5); + + using (var stream = new MemoryStream ()) { + var options = FormatOptions.Default.Clone (); + options.NewLineFormat = NewLineFormat.Dos; + + message.WriteTo (options, stream); + length = stream.Length; + stream.Position = 0; + + using (var reader = new StreamReader (stream, Latin1)) + latin1 = reader.ReadToEnd (); + } + + if (withKeywords) + command.Append (" (\\Seen $NotJunk) "); + else + command.Append (" (\\Seen) "); + + if (withInternalDates) + command.AppendFormat ("\"{0}\" ", ImapUtils.FormatInternalDate (message.Date)); + + command.Append ('{'); + command.AppendFormat ("{0}+", length); + command.Append ("}\r\n"); + command.Append (latin1); + command.Append ("\r\n"); + commands.Add (new ImapReplayCommand (command.ToString (), string.Format ("dovecot.append.{0}.txt", i + 1))); + } + + commands.Add (new ImapReplayCommand ("A00000013 LOGOUT\r\n", "gmail.logout.txt")); + + return commands; + } + + [TestCase (false, false, TestName = "TestMultiAppend")] + [TestCase (true, false, TestName = "TestMultiAppendWithKeywords")] + [TestCase (false, true, TestName = "TestMultiAppendWithInternalDates")] + [TestCase (true, true, TestName = "TestMultiAppendWithKeywordsAndInternalDates")] + public void TestMultiAppend (bool withKeywords, bool withInternalDates) + { + var commands = CreateMultiAppendCommands (withKeywords, withInternalDates, out var messages, out var flags, out var keywords, out var internalDates); + IList uids; + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities.HasFlag (ImapCapabilities.MultiAppend), Is.True, "MULTIAPPEND"); + + // Use MULTIAPPEND to append some test messages + if (withKeywords) { + var requests = new List (); + + for (int i = 0; i < messages.Count; i++) { + if (withInternalDates) { + requests.Add (new AppendRequest (messages[i], flags[i], keywords[i], internalDates[i])); + } else { + requests.Add (new AppendRequest (messages[i], flags[i], keywords[i])); + } + } + + uids = client.Inbox.Append (requests); + } else if (withInternalDates) { + uids = client.Inbox.Append (messages, flags, internalDates); + } else { + uids = client.Inbox.Append (messages, flags); + } + + Assert.That (uids, Has.Count.EqualTo (8), "Unexpected number of messages appended"); + + for (int i = 0; i < uids.Count; i++) + Assert.That (uids[i].Id, Is.EqualTo (i + 1), "Unexpected UID"); + + // Disable the MULTIAPPEND extension and do it again + client.Capabilities &= ~ImapCapabilities.MultiAppend; + + if (withKeywords) { + var requests = new List (); + + for (int i = 0; i < messages.Count; i++) { + if (withInternalDates) { + requests.Add (new AppendRequest (messages[i], flags[i], keywords[i], internalDates[i])); + } else { + requests.Add (new AppendRequest (messages[i], flags[i], keywords[i])); + } + } + + uids = client.Inbox.Append (requests); + } else if (withInternalDates) { + uids = client.Inbox.Append (messages, flags, internalDates); + } else { + uids = client.Inbox.Append (messages, flags); + } + + Assert.That (uids, Has.Count.EqualTo (8), "Unexpected number of messages appended"); + + for (int i = 0; i < uids.Count; i++) + Assert.That (uids[i].Id, Is.EqualTo (i + 1), "Unexpected UID"); + + client.Disconnect (true); + + foreach (var message in messages) + message.Dispose (); + } + } + + [TestCase (false, false, TestName = "TestMultiAppendAsync")] + [TestCase (true, false, TestName = "TestMultiAppendWithKeywordsAsync")] + [TestCase (false, true, TestName = "TestMultiAppendWithInternalDatesAsync")] + [TestCase (true, true, TestName = "TestMultiAppendWithKeywordsAndInternalDatesAsync")] + public async Task TestMultiAppendAsync (bool withKeywords, bool withInternalDates) + { + var commands = CreateMultiAppendCommands (withKeywords, withInternalDates, out var messages, out var flags, out var keywords, out var internalDates); + IList uids; + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities.HasFlag (ImapCapabilities.MultiAppend), Is.True, "MULTIAPPEND"); + + // Use MULTIAPPEND to append some test messages + if (withKeywords) { + var requests = new List (); + + for (int i = 0; i < messages.Count; i++) { + if (withInternalDates) { + requests.Add (new AppendRequest (messages[i], flags[i], keywords[i], internalDates[i])); + } else { + requests.Add (new AppendRequest (messages[i], flags[i], keywords[i])); + } + } + + uids = await client.Inbox.AppendAsync (requests); + } else if (withInternalDates) { + uids = await client.Inbox.AppendAsync (messages, flags, internalDates); + } else { + uids = await client.Inbox.AppendAsync (messages, flags); + } + + Assert.That (uids, Has.Count.EqualTo (8), "Unexpected number of messages appended"); + + for (int i = 0; i < uids.Count; i++) + Assert.That (uids[i].Id, Is.EqualTo (i + 1), "Unexpected UID"); + + // Disable the MULTIAPPEND extension and do it again + client.Capabilities &= ~ImapCapabilities.MultiAppend; + + if (withKeywords) { + var requests = new List (); + + for (int i = 0; i < messages.Count; i++) { + if (withInternalDates) { + requests.Add (new AppendRequest (messages[i], flags[i], keywords[i], internalDates[i])); + } else { + requests.Add (new AppendRequest (messages[i], flags[i], keywords[i])); + } + } + + uids = await client.Inbox.AppendAsync (requests); + } else if (withInternalDates) { + uids = await client.Inbox.AppendAsync (messages, flags, internalDates); + } else { + uids = await client.Inbox.AppendAsync (messages, flags); + } + + Assert.That (uids, Has.Count.EqualTo (8), "Unexpected number of messages appended"); + + for (int i = 0; i < uids.Count; i++) + Assert.That (uids[i].Id, Is.EqualTo (i + 1), "Unexpected UID"); + + await client.DisconnectAsync (true); + + foreach (var message in messages) + message.Dispose (); + } + } + + static List CreateReplaceCommands (bool clientSide, bool withKeywords, bool withInternalDates, out List messages, out List flags, out List> keywords, out List internalDates) + { + var commands = new List { + new ImapReplayCommand ("", "dovecot.greeting.txt"), + new ImapReplayCommand ("A00000000 LOGIN username password\r\n", "dovecot.authenticate+replace.txt"), + new ImapReplayCommand ("A00000001 NAMESPACE\r\n", "dovecot.namespace.txt"), + new ImapReplayCommand ("A00000002 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-inbox.txt"), + new ImapReplayCommand ("A00000003 LIST (SPECIAL-USE) \"\" \"*\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-special-use.txt"), + new ImapReplayCommand ("A00000004 SELECT INBOX (CONDSTORE)\r\n", "common.select-inbox.txt") + }; + + internalDates = withInternalDates ? new List () : null; + keywords = withKeywords ? new List> () : null; + messages = new List (); + flags = new List (); + var command = new StringBuilder (); + int id = 5; + + for (int i = 0; i < 8; i++) { + MimeMessage message; + string latin1; + long length; + + using (var resource = GetResourceStream (string.Format ("common.message.{0}.msg", i))) + message = MimeMessage.Load (resource); + + messages.Add (message); + + flags.Add (MessageFlags.Seen); + + if (withKeywords) + keywords.Add (new List { "$NotJunk" }); + + if (withInternalDates) + internalDates.Add (message.Date); + + using (var stream = new MemoryStream ()) { + var options = FormatOptions.Default.Clone (); + options.NewLineFormat = NewLineFormat.Dos; + options.EnsureNewLine = true; + + message.WriteTo (options, stream); + length = stream.Length; + stream.Position = 0; + + using (var reader = new StreamReader (stream, Latin1)) + latin1 = reader.ReadToEnd (); + } + + var tag = string.Format ("A{0:D8}", id++); + command.Clear (); + + if (clientSide) + command.AppendFormat ("{0} APPEND INBOX (\\Seen", tag); + else + command.AppendFormat ("{0} REPLACE {1} INBOX (\\Seen", tag, i + 1); + + if (withKeywords) + command.Append (" $NotJunk) "); + else + command.Append (") "); + + if (withInternalDates) + command.AppendFormat ("\"{0}\" ", ImapUtils.FormatInternalDate (message.Date)); + + //if (length > 4096) { + // command.Append ('{').Append (length.ToString ()).Append ("}\r\n"); + // commands.Add (new ImapReplayCommand (command.ToString (), ImapReplayCommandResponse.Plus)); + // commands.Add (new ImapReplayCommand (tag, latin1 + "\r\n", string.Format ("dovecot.append.{0}.txt", i + 1))); + //} else { + command.Append ('{').Append (length.ToString (CultureInfo.InvariantCulture)).Append ("+}\r\n").Append (latin1).Append ("\r\n"); + commands.Add (new ImapReplayCommand (command.ToString (), string.Format ("dovecot.append.{0}.txt", i + 1))); + //} + + if (clientSide) { + tag = string.Format ("A{0:D8}", id++); + commands.Add (new ImapReplayCommand ($"{tag} STORE {i + 1} +FLAGS.SILENT (\\Deleted)\r\n", ImapReplayCommandResponse.OK)); + } + } + + commands.Add (new ImapReplayCommand (string.Format ("A{0:D8} LOGOUT\r\n", id), "gmail.logout.txt")); + + return commands; + } + + [TestCase (false, false, false, TestName = "TestReplace")] + [TestCase (false, true, false, TestName = "TestReplaceWithKeywords")] + [TestCase (false, false, true, TestName = "TestReplaceWithInternalDates")] + [TestCase (false, true, true, TestName = "TestReplaceWithKeywordsAndInternalDates")] + [TestCase (true, false, false, TestName = "TestClientSideReplace")] + [TestCase (true, true, false, TestName = "TestClientSideReplaceWithKeywords")] + [TestCase (true, false, true, TestName = "TestClientSideReplaceWithInternalDates")] + [TestCase (true, true, true, TestName = "TestClientSideReplaceWithKeywordsAndInternalDates")] + public void TestReplace (bool clientSide, bool withKeywords, bool withInternalDates) + { + var commands = CreateReplaceCommands (clientSide, withKeywords, withInternalDates, out var messages, out var flags, out var keywords, out var internalDates); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + if (clientSide) + client.Capabilities &= ~ImapCapabilities.Replace; + else + Assert.That (client.Capabilities.HasFlag (ImapCapabilities.Replace), Is.True, "REPLACE"); + + client.Inbox.Open (FolderAccess.ReadWrite); + + for (int i = 0; i < messages.Count; i++) { + UniqueId? uid; + + if (withKeywords) { + ReplaceRequest request; + + if (withInternalDates) { + request = new ReplaceRequest (messages[i], flags[i], keywords[i], internalDates[i]); + } else { + request = new ReplaceRequest (messages[i], flags[i], keywords[i]); + } + + uid = client.Inbox.Replace (i, request); + } else if (withInternalDates) { + uid = client.Inbox.Replace (i, messages[i], flags[i], internalDates[i]); + } else { + uid = client.Inbox.Replace (i, messages[i], flags[i]); + } + + Assert.That (uid.HasValue, Is.True, "Expected a UIDAPPEND resp-code"); + Assert.That (uid.Value.Id, Is.EqualTo (i + 1), "Unexpected UID"); + } + + client.Disconnect (true); + + foreach (var message in messages) + message.Dispose (); + } + } + + [TestCase (false, false, false, TestName = "TestReplaceAsync")] + [TestCase (false, true, false, TestName = "TestReplaceWithKeywordsAsync")] + [TestCase (false, false, true, TestName = "TestReplaceWithInternalDatesAsync")] + [TestCase (false, true, true, TestName = "TestReplaceWithKeywordsAndInternalDatesAsync")] + [TestCase (true, false, false, TestName = "TestClientSideReplaceAsync")] + [TestCase (true, true, false, TestName = "TestClientSideReplaceWithKeywordsAsync")] + [TestCase (true, false, true, TestName = "TestClientSideReplaceWithInternalDatesAsync")] + [TestCase (true, true, true, TestName = "TestClientSideReplaceWithKeywordsAndInternalDatesAsync")] + public async Task TestReplaceAsync (bool clientSide, bool withKeywords, bool withInternalDates) + { + var commands = CreateReplaceCommands (clientSide, withKeywords, withInternalDates, out var messages, out var flags, out var keywords, out var internalDates); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + if (clientSide) + client.Capabilities &= ~ImapCapabilities.Replace; + else + Assert.That (client.Capabilities.HasFlag (ImapCapabilities.Replace), Is.True, "REPLACE"); + + await client.Inbox.OpenAsync (FolderAccess.ReadWrite); + + for (int i = 0; i < messages.Count; i++) { + UniqueId? uid; + + if (withKeywords) { + ReplaceRequest request; + + if (withInternalDates) { + request = new ReplaceRequest (messages[i], flags[i], keywords[i], internalDates[i]); + } else { + request = new ReplaceRequest (messages[i], flags[i], keywords[i]); + } + + uid = await client.Inbox.ReplaceAsync (i, request); + } else if (withInternalDates) { + uid = await client.Inbox.ReplaceAsync (i, messages[i], flags[i], internalDates[i]); + } else { + uid = await client.Inbox.ReplaceAsync (i, messages[i], flags[i]); + } + + Assert.That (uid.HasValue, Is.True, "Expected a UIDAPPEND resp-code"); + Assert.That (uid.Value.Id, Is.EqualTo (i + 1), "Unexpected UID"); + } + + await client.DisconnectAsync (true); + + foreach (var message in messages) + message.Dispose (); + } + } + + static List CreateReplaceByUidCommands (bool clientSide, bool withKeywords, bool withInternalDates, out List messages, out List flags, out List> keywords, out List internalDates) + { + var commands = new List { + new ImapReplayCommand ("", "dovecot.greeting.txt"), + new ImapReplayCommand ("A00000000 LOGIN username password\r\n", "dovecot.authenticate+replace.txt"), + new ImapReplayCommand ("A00000001 NAMESPACE\r\n", "dovecot.namespace.txt"), + new ImapReplayCommand ("A00000002 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-inbox.txt"), + new ImapReplayCommand ("A00000003 LIST (SPECIAL-USE) \"\" \"*\" RETURN (SUBSCRIBED CHILDREN)\r\n", "dovecot.list-special-use.txt"), + new ImapReplayCommand ("A00000004 SELECT INBOX (CONDSTORE)\r\n", "common.select-inbox.txt") + }; + + internalDates = withInternalDates ? new List () : null; + keywords = withKeywords ? new List> () : null; + messages = new List (); + flags = new List (); + var command = new StringBuilder (); + int id = 5; + + for (int i = 0; i < 8; i++) { + MimeMessage message; + string latin1; + long length; + + using (var resource = GetResourceStream (string.Format ("common.message.{0}.msg", i))) + message = MimeMessage.Load (resource); + + messages.Add (message); + + flags.Add (MessageFlags.Seen); + + if (withKeywords) + keywords.Add (new List { "$NotJunk" }); + + if (withInternalDates) + internalDates.Add (message.Date); + + using (var stream = new MemoryStream ()) { + var options = FormatOptions.Default.Clone (); + options.NewLineFormat = NewLineFormat.Dos; + options.EnsureNewLine = true; + + message.WriteTo (options, stream); + length = stream.Length; + stream.Position = 0; + + using (var reader = new StreamReader (stream, Latin1)) + latin1 = reader.ReadToEnd (); + } + + var tag = string.Format ("A{0:D8}", id++); + command.Clear (); + + if (clientSide) + command.AppendFormat ("{0} APPEND INBOX (\\Seen", tag); + else + command.AppendFormat ("{0} UID REPLACE {1} INBOX (\\Seen", tag, i + 1); + + if (withKeywords) + command.Append (" $NotJunk) "); + else + command.Append (") "); + + if (withInternalDates) + command.AppendFormat ("\"{0}\" ", ImapUtils.FormatInternalDate (message.Date)); + + //if (length > 4096) { + // command.Append ('{').Append (length.ToString ()).Append ("}\r\n"); + // commands.Add (new ImapReplayCommand (command.ToString (), ImapReplayCommandResponse.Plus)); + // commands.Add (new ImapReplayCommand (tag, latin1 + "\r\n", string.Format ("dovecot.append.{0}.txt", i + 1))); + //} else { + command.Append ('{').Append (length.ToString (CultureInfo.InvariantCulture)).Append ("+}\r\n").Append (latin1).Append ("\r\n"); + commands.Add (new ImapReplayCommand (command.ToString (), string.Format ("dovecot.append.{0}.txt", i + 1))); + //} + + if (clientSide) { + tag = string.Format ("A{0:D8}", id++); + commands.Add (new ImapReplayCommand ($"{tag} UID STORE {i + 1} +FLAGS.SILENT (\\Deleted)\r\n", ImapReplayCommandResponse.OK)); + + tag = string.Format ("A{0:D8}", id++); + commands.Add (new ImapReplayCommand ($"{tag} UID EXPUNGE {i + 1}\r\n", ImapReplayCommandResponse.OK)); + } + } + + commands.Add (new ImapReplayCommand (string.Format ("A{0:D8} LOGOUT\r\n", id), "gmail.logout.txt")); + + return commands; + } + + [TestCase (false, false, false, TestName = "TestReplaceByUid")] + [TestCase (false, true, false, TestName = "TestReplaceByUidWithKeywords")] + [TestCase (false, false, true, TestName = "TestReplaceByUidWithInternalDates")] + [TestCase (false, true, true, TestName = "TestReplaceByUidWithKeywordsAndInternalDates")] + [TestCase (true, false, false, TestName = "TestClientSideReplaceByUid")] + [TestCase (true, true, false, TestName = "TestClientSideReplaceByUidWithKeywords")] + [TestCase (true, false, true, TestName = "TestClientSideReplaceByUidWithInternalDates")] + [TestCase (true, true, true, TestName = "TestClientSideReplaceByUidWithKeywordsAndInternalDates")] + public void TestReplaceByUid (bool clientSide, bool withKeywords, bool withInternalDates) + { + var commands = CreateReplaceByUidCommands (clientSide, withKeywords, withInternalDates, out var messages, out var flags, out var keywords, out var internalDates); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + if (clientSide) + client.Capabilities &= ~ImapCapabilities.Replace; + else + Assert.That (client.Capabilities.HasFlag (ImapCapabilities.Replace), Is.True, "REPLACE"); + + client.Inbox.Open (FolderAccess.ReadWrite); + + for (int i = 0; i < messages.Count; i++) { + UniqueId? uid; + + if (withKeywords) { + ReplaceRequest request; + + if (withInternalDates) { + request = new ReplaceRequest (messages[i], flags[i], keywords[i], internalDates[i]); + } else { + request = new ReplaceRequest (messages[i], flags[i], keywords[i]); + } + + uid = client.Inbox.Replace (new UniqueId ((uint) i + 1), request); + } else if (withInternalDates) { + uid = client.Inbox.Replace (new UniqueId ((uint) i + 1), messages[i], flags[i], internalDates[i]); + } else { + uid = client.Inbox.Replace (new UniqueId ((uint) i + 1), messages[i], flags[i]); + } + + Assert.That (uid.HasValue, Is.True, "Expected a UIDAPPEND resp-code"); + Assert.That (uid.Value.Id, Is.EqualTo (i + 1), "Unexpected UID"); + } + + client.Disconnect (true); + + foreach (var message in messages) + message.Dispose (); + } + } + + [TestCase (false, false, false, TestName = "TestReplaceByUidAsync")] + [TestCase (false, true, false, TestName = "TestReplaceByUidWithKeywordsAsync")] + [TestCase (false, false, true, TestName = "TestReplaceByUidWithInternalDatesAsync")] + [TestCase (false, true, true, TestName = "TestReplaceByUidWithKeywordsAndInternalDatesAsync")] + [TestCase (true, false, false, TestName = "TestClientSideReplaceByUidAsync")] + [TestCase (true, true, false, TestName = "TestClientSideReplaceByUidWithKeywordsAsync")] + [TestCase (true, false, true, TestName = "TestClientSideReplaceByUidWithInternalDatesAsync")] + [TestCase (true, true, true, TestName = "TestClientSideReplaceByUidWithKeywordsAndInternalDatesAsync")] + public async Task TestReplaceByUidAsync (bool clientSide, bool withKeywords, bool withInternalDates) + { + var commands = CreateReplaceByUidCommands (clientSide, withKeywords, withInternalDates, out var messages, out var flags, out var keywords, out var internalDates); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + // Note: we do not want to use SASL at all... + client.AuthenticationMechanisms.Clear (); + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + if (clientSide) + client.Capabilities &= ~ImapCapabilities.Replace; + else + Assert.That (client.Capabilities.HasFlag (ImapCapabilities.Replace), Is.True, "REPLACE"); + + await client.Inbox.OpenAsync (FolderAccess.ReadWrite); + + for (int i = 0; i < messages.Count; i++) { + UniqueId? uid; + + if (withKeywords) { + ReplaceRequest request; + + if (withInternalDates) { + request = new ReplaceRequest (messages[i], flags[i], keywords[i], internalDates[i]); + } else { + request = new ReplaceRequest (messages[i], flags[i], keywords[i]); + } + + uid = await client.Inbox.ReplaceAsync (new UniqueId ((uint) i + 1), request); + } else if (withInternalDates) { + uid = await client.Inbox.ReplaceAsync (new UniqueId ((uint) i + 1), messages[i], flags[i], internalDates[i]); + } else { + uid = await client.Inbox.ReplaceAsync (new UniqueId ((uint) i + 1), messages[i], flags[i]); + } + + Assert.That (uid.HasValue, Is.True, "Expected a UIDAPPEND resp-code"); + Assert.That (uid.Value.Id, Is.EqualTo (i + 1), "Unexpected UID"); + } + + await client.DisconnectAsync (true); + + foreach (var message in messages) + message.Dispose (); + } + } + + static List CreateCreateRenameDeleteCommands () + { + return new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt"), + new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "gmail.authenticate.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-inbox.txt"), + new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt"), + new ImapReplayCommand ("A00000005 CREATE TopLevel1\r\n", ImapReplayCommandResponse.OK), + new ImapReplayCommand ("A00000006 LIST \"\" TopLevel1\r\n", "gmail.list-toplevel1.txt"), + new ImapReplayCommand ("A00000007 CREATE TopLevel2\r\n", ImapReplayCommandResponse.OK), + new ImapReplayCommand ("A00000008 LIST \"\" TopLevel2\r\n", "gmail.list-toplevel2.txt"), + new ImapReplayCommand ("A00000009 CREATE TopLevel1/SubLevel1\r\n", ImapReplayCommandResponse.OK), + new ImapReplayCommand ("A00000010 LIST \"\" TopLevel1/SubLevel1\r\n", "gmail.list-sublevel1.txt"), + new ImapReplayCommand ("A00000011 CREATE TopLevel2/SubLevel2\r\n", ImapReplayCommandResponse.OK), + new ImapReplayCommand ("A00000012 LIST \"\" TopLevel2/SubLevel2\r\n", "gmail.list-sublevel2.txt"), + new ImapReplayCommand ("A00000013 SELECT TopLevel1/SubLevel1 (CONDSTORE)\r\n", "gmail.select-sublevel1.txt"), + new ImapReplayCommand ("A00000014 RENAME TopLevel1/SubLevel1 TopLevel2/SubLevel1\r\n", ImapReplayCommandResponse.OK), + new ImapReplayCommand ("A00000015 DELETE TopLevel1\r\n", ImapReplayCommandResponse.OK), + new ImapReplayCommand ("A00000016 SELECT TopLevel2/SubLevel2 (CONDSTORE)\r\n", "gmail.select-sublevel2.txt"), + new ImapReplayCommand ("A00000017 RENAME TopLevel2 TopLevel\r\n", ImapReplayCommandResponse.OK), + new ImapReplayCommand ("A00000018 SELECT TopLevel (CONDSTORE)\r\n", "gmail.select-toplevel.txt"), + new ImapReplayCommand ("A00000019 DELETE TopLevel\r\n", ImapReplayCommandResponse.OK), + new ImapReplayCommand ("A00000020 LOGOUT\r\n", "gmail.logout.txt") + }; + } + + [Test] + public void TestCreateRenameDelete () + { + var commands = CreateCreateRenameDeleteCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + int top1Renamed = 0, top2Renamed = 0, sub1Renamed = 0, sub2Renamed = 0; + int top1Deleted = 0, top2Deleted = 0, sub1Deleted = 0, sub2Deleted = 0; + int top1Closed = 0, top2Closed = 0, sub1Closed = 0, sub2Closed = 0; + var personal = client.GetFolder (client.PersonalNamespaces[0]); + var toplevel1 = personal.Create ("TopLevel1", false); + var toplevel2 = personal.Create ("TopLevel2", false); + var sublevel1 = toplevel1.Create ("SubLevel1", true); + var sublevel2 = toplevel2.Create ("SubLevel2", true); + + toplevel1.Renamed += (o, e) => { top1Renamed++; }; + toplevel2.Renamed += (o, e) => { top2Renamed++; }; + sublevel1.Renamed += (o, e) => { sub1Renamed++; }; + sublevel2.Renamed += (o, e) => { sub2Renamed++; }; + + toplevel1.Deleted += (o, e) => { top1Deleted++; }; + toplevel2.Deleted += (o, e) => { top2Deleted++; }; + sublevel1.Deleted += (o, e) => { sub1Deleted++; }; + sublevel2.Deleted += (o, e) => { sub2Deleted++; }; + + toplevel1.Closed += (o, e) => { top1Closed++; }; + toplevel2.Closed += (o, e) => { top2Closed++; }; + sublevel1.Closed += (o, e) => { sub1Closed++; }; + sublevel2.Closed += (o, e) => { sub2Closed++; }; + + Assert.That (sublevel1.CanOpen, Is.True, "SubLevel1 can be opened"); + sublevel1.Open (FolderAccess.ReadWrite); + sublevel1.Rename (toplevel2, "SubLevel1"); + + Assert.That (sub1Renamed, Is.EqualTo (1), "SubLevel1 folder should have received a Renamed event"); + Assert.That (sub1Closed, Is.EqualTo (1), "SubLevel1 should have received a Closed event"); + Assert.That (sublevel1.IsOpen, Is.False, "SubLevel1 should be closed after being renamed"); + + toplevel1.Delete (); + Assert.That (top1Deleted, Is.EqualTo (1), "TopLevel1 should have received a Deleted event"); + Assert.That (toplevel1.Exists, Is.False, "TopLevel1.Exists"); + + Assert.That (sublevel2.CanOpen, Is.True, "SubLevel2 can be opened"); + sublevel2.Open (FolderAccess.ReadWrite); + toplevel2.Rename (personal, "TopLevel"); + + Assert.That (sub1Renamed, Is.EqualTo (2), "SubLevel1 folder should have received a Renamed event"); + Assert.That (sub2Renamed, Is.EqualTo (1), "SubLevel2 folder should have received a Renamed event"); + Assert.That (sub2Closed, Is.EqualTo (1), "SubLevel2 should have received a Closed event"); + Assert.That (sublevel2.IsOpen, Is.False, "SubLevel2 should be closed after being renamed"); + Assert.That (top2Renamed, Is.EqualTo (1), "TopLevel2 folder should have received a Renamed event"); + + toplevel2.Open (FolderAccess.ReadWrite); + toplevel2.Delete (); + Assert.That (top2Closed, Is.EqualTo (1), "TopLevel2 should have received a Closed event"); + Assert.That (toplevel2.IsOpen, Is.False, "TopLevel2 should be closed after being deleted"); + Assert.That (top2Deleted, Is.EqualTo (1), "TopLevel2 should have received a Deleted event"); + Assert.That (toplevel2.Exists, Is.False, "TopLevel2.Exists"); + + client.Disconnect (true); + } + } + + [Test] + public async Task TestCreateRenameDeleteAsync () + { + var commands = CreateCreateRenameDeleteCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + int top1Renamed = 0, top2Renamed = 0, sub1Renamed = 0, sub2Renamed = 0; + int top1Deleted = 0, top2Deleted = 0, sub1Deleted = 0, sub2Deleted = 0; + int top1Closed = 0, top2Closed = 0, sub1Closed = 0, sub2Closed = 0; + var personal = client.GetFolder (client.PersonalNamespaces[0]); + var toplevel1 = await personal.CreateAsync ("TopLevel1", false); + var toplevel2 = await personal.CreateAsync ("TopLevel2", false); + var sublevel1 = await toplevel1.CreateAsync ("SubLevel1", true); + var sublevel2 = await toplevel2.CreateAsync ("SubLevel2", true); + + toplevel1.Renamed += (o, e) => { top1Renamed++; }; + toplevel2.Renamed += (o, e) => { top2Renamed++; }; + sublevel1.Renamed += (o, e) => { sub1Renamed++; }; + sublevel2.Renamed += (o, e) => { sub2Renamed++; }; + + toplevel1.Deleted += (o, e) => { top1Deleted++; }; + toplevel2.Deleted += (o, e) => { top2Deleted++; }; + sublevel1.Deleted += (o, e) => { sub1Deleted++; }; + sublevel2.Deleted += (o, e) => { sub2Deleted++; }; + + toplevel1.Closed += (o, e) => { top1Closed++; }; + toplevel2.Closed += (o, e) => { top2Closed++; }; + sublevel1.Closed += (o, e) => { sub1Closed++; }; + sublevel2.Closed += (o, e) => { sub2Closed++; }; + + Assert.That (sublevel1.CanOpen, Is.True, "SubLevel1 can be opened"); + await sublevel1.OpenAsync (FolderAccess.ReadWrite); + await sublevel1.RenameAsync (toplevel2, "SubLevel1"); + + Assert.That (sub1Renamed, Is.EqualTo (1), "SubLevel1 folder should have received a Renamed event"); + Assert.That (sub1Closed, Is.EqualTo (1), "SubLevel1 should have received a Closed event"); + Assert.That (sublevel1.IsOpen, Is.False, "SubLevel1 should be closed after being renamed"); + + await toplevel1.DeleteAsync (); + Assert.That (top1Deleted, Is.EqualTo (1), "TopLevel1 should have received a Deleted event"); + Assert.That (toplevel1.Exists, Is.False, "TopLevel1.Exists"); + + Assert.That (sublevel2.CanOpen, Is.True, "SubLevel2 can be opened"); + await sublevel2.OpenAsync (FolderAccess.ReadWrite); + await toplevel2.RenameAsync (personal, "TopLevel"); + + Assert.That (sub1Renamed, Is.EqualTo (2), "SubLevel1 folder should have received a Renamed event"); + Assert.That (sub2Renamed, Is.EqualTo (1), "SubLevel2 folder should have received a Renamed event"); + Assert.That (sub2Closed, Is.EqualTo (1), "SubLevel2 should have received a Closed event"); + Assert.That (sublevel2.IsOpen, Is.False, "SubLevel2 should be closed after being renamed"); + Assert.That (top2Renamed, Is.EqualTo (1), "TopLevel2 folder should have received a Renamed event"); + + await toplevel2.OpenAsync (FolderAccess.ReadWrite); + await toplevel2.DeleteAsync (); + Assert.That (top2Closed, Is.EqualTo (1), "TopLevel2 should have received a Closed event"); + Assert.That (toplevel2.IsOpen, Is.False, "TopLevel2 should be closed after being deleted"); + Assert.That (top2Deleted, Is.EqualTo (1), "TopLevel2 should have received a Deleted event"); + Assert.That (toplevel2.Exists, Is.False, "TopLevel2.Exists"); + + await client.DisconnectAsync (true); + } + } + + static List CreateCreateMailboxIdCommands () + { + return new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt"), + new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "gmail.authenticate+create-special-use.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-inbox.txt"), + new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt"), + new ImapReplayCommand ("A00000005 CREATE TopLevel1\r\n", "gmail.create-mailboxid.txt"), + new ImapReplayCommand ("A00000006 LIST \"\" TopLevel1\r\n", "gmail.list-toplevel1.txt"), + new ImapReplayCommand ("A00000007 LOGOUT\r\n", "gmail.logout.txt") + }; + } + + [Test] + public void TestCreateMailboxId () + { + var commands = CreateCreateMailboxIdCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities.HasFlag (ImapCapabilities.ObjectID), Is.True, "OBJECTID"); + + var personal = client.GetFolder (client.PersonalNamespaces[0]); + var toplevel1 = personal.Create ("TopLevel1", true); + Assert.That (toplevel1.Attributes, Is.EqualTo (FolderAttributes.HasNoChildren)); + Assert.That (toplevel1.Id, Is.EqualTo ("25dcfa84-fd65-41c3-abc3-633c8f10923f")); + + client.Disconnect (true); + } + } + + [Test] + public async Task TestCreateMailboxIdAsync () + { + var commands = CreateCreateMailboxIdCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities.HasFlag (ImapCapabilities.ObjectID), Is.True, "OBJECTID"); + + var personal = client.GetFolder (client.PersonalNamespaces[0]); + var toplevel1 = await personal.CreateAsync ("TopLevel1", true); + Assert.That (toplevel1.Attributes, Is.EqualTo (FolderAttributes.HasNoChildren)); + Assert.That (toplevel1.Id, Is.EqualTo ("25dcfa84-fd65-41c3-abc3-633c8f10923f")); + + await client.DisconnectAsync (true); + } + } + + static List CreateCreateSpecialUseCommands () + { + return new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt"), + new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "gmail.authenticate+create-special-use.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-inbox.txt"), + new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt"), + new ImapReplayCommand ("A00000005 CREATE \"[Gmail]/Archives\" (USE (\\Archive))\r\n", "gmail.create-mailboxid.txt"), + new ImapReplayCommand ("A00000006 LIST \"\" \"[Gmail]/Archives\"\r\n", "gmail.list-archives.txt"), + new ImapReplayCommand ("A00000007 CREATE \"[Gmail]/Flagged\" (USE (\\Flagged))\r\n", "gmail.create-mailboxid.txt"), + new ImapReplayCommand ("A00000008 LIST \"\" \"[Gmail]/Flagged\"\r\n", "gmail.list-flagged.txt"), + new ImapReplayCommand ("A00000009 LOGOUT\r\n", "gmail.logout.txt") + }; + } + + [Test] + public void TestCreateSpecialUse () + { + var commands = CreateCreateSpecialUseCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities.HasFlag (ImapCapabilities.CreateSpecialUse), Is.True, "CREATE-SPECIAL-USE"); + + var personal = client.GetFolder (client.PersonalNamespaces[0]); + var gmail = personal.GetSubfolder ("[Gmail]"); + + var archives = gmail.Create ("Archives", SpecialFolder.Archive); + Assert.That (archives.Attributes, Is.EqualTo (FolderAttributes.HasNoChildren | FolderAttributes.Archive)); + Assert.That (client.GetFolder (SpecialFolder.Archive), Is.EqualTo (archives)); + Assert.That (archives.Id, Is.EqualTo ("25dcfa84-fd65-41c3-abc3-633c8f10923f")); + + var flagged = gmail.Create ("Flagged", SpecialFolder.Flagged); + Assert.That (flagged.Attributes, Is.EqualTo (FolderAttributes.HasNoChildren | FolderAttributes.Flagged)); + Assert.That (client.GetFolder (SpecialFolder.Flagged), Is.EqualTo (flagged)); + Assert.That (flagged.Id, Is.EqualTo ("25dcfa84-fd65-41c3-abc3-633c8f10923f")); + + client.Disconnect (true); + } + } + + [Test] + public async Task TestCreateSpecialUseAsync () + { + var commands = CreateCreateSpecialUseCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities.HasFlag (ImapCapabilities.CreateSpecialUse), Is.True, "CREATE-SPECIAL-USE"); + + var personal = client.GetFolder (client.PersonalNamespaces[0]); + var gmail = await personal.GetSubfolderAsync ("[Gmail]"); + + var archives = await gmail.CreateAsync ("Archives", SpecialFolder.Archive); + Assert.That (archives.Attributes, Is.EqualTo (FolderAttributes.HasNoChildren | FolderAttributes.Archive)); + Assert.That (client.GetFolder (SpecialFolder.Archive), Is.EqualTo (archives)); + Assert.That (archives.Id, Is.EqualTo ("25dcfa84-fd65-41c3-abc3-633c8f10923f")); + + var flagged = await gmail.CreateAsync ("Flagged", SpecialFolder.Flagged); + Assert.That (flagged.Attributes, Is.EqualTo (FolderAttributes.HasNoChildren | FolderAttributes.Flagged)); + Assert.That (client.GetFolder (SpecialFolder.Flagged), Is.EqualTo (flagged)); + Assert.That (flagged.Id, Is.EqualTo ("25dcfa84-fd65-41c3-abc3-633c8f10923f")); + + await client.DisconnectAsync (true); + } + } + + static List CreateCreateSpecialUseMultipleCommands () + { + return new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt"), + new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "gmail.authenticate+create-special-use.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-inbox.txt"), + new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt"), + new ImapReplayCommand ("A00000005 CREATE \"[Gmail]/Archives\" (USE (\\All \\Archive \\Drafts \\Flagged \\Important \\Junk \\Sent \\Trash))\r\n", "gmail.create-mailboxid.txt"), + new ImapReplayCommand ("A00000006 LIST \"\" \"[Gmail]/Archives\"\r\n", "gmail.list-archives.txt"), + new ImapReplayCommand ("A00000007 CREATE \"[Gmail]/MyImportant\" (USE (\\Important))\r\n", Encoding.ASCII.GetBytes ("A00000007 NO [USEATTR] An \\Important mailbox already exists\r\n")), + new ImapReplayCommand ("A00000008 LOGOUT\r\n", "gmail.logout.txt") + }; + } + + [Test] + public void TestCreateSpecialUseMultiple () + { + var commands = CreateCreateSpecialUseMultipleCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities.HasFlag (ImapCapabilities.CreateSpecialUse), Is.True, "CREATE-SPECIAL-USE"); + + var personal = client.GetFolder (client.PersonalNamespaces[0]); + var gmail = personal.GetSubfolder ("[Gmail]"); + + var uses = new List { + SpecialFolder.All, + SpecialFolder.Archive, + SpecialFolder.Drafts, + SpecialFolder.Flagged, + SpecialFolder.Important, + SpecialFolder.Junk, + SpecialFolder.Sent, + SpecialFolder.Trash, + + // specifically duplicate some special uses + SpecialFolder.All, + SpecialFolder.Flagged, + + // and add one that is invalid + (SpecialFolder) 15 + }; + + var archive = gmail.Create ("Archives", uses); + Assert.That (archive.Attributes, Is.EqualTo (FolderAttributes.HasNoChildren | FolderAttributes.Archive)); + Assert.That (client.GetFolder (SpecialFolder.Archive), Is.EqualTo (archive)); + Assert.That (archive.Id, Is.EqualTo ("25dcfa84-fd65-41c3-abc3-633c8f10923f")); + + try { + gmail.Create ("MyImportant", new[] { SpecialFolder.Important }); + Assert.Fail ("Creating the MyImportant folder should have thrown an ImapCommandException"); + } catch (ImapCommandException ex) { + Assert.That (ex.Response, Is.EqualTo (ImapCommandResponse.No)); + Assert.That (ex.ResponseText, Is.EqualTo ("An \\Important mailbox already exists")); + } catch (Exception ex) { + Assert.Fail ($"Unexpected exception: {ex}"); + } + + client.Disconnect (true); + } + } + + [Test] + public async Task TestCreateSpecialUseMultipleAsync () + { + var commands = CreateCreateSpecialUseMultipleCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities.HasFlag (ImapCapabilities.CreateSpecialUse), Is.True, "CREATE-SPECIAL-USE"); + + var personal = client.GetFolder (client.PersonalNamespaces[0]); + var gmail = await personal.GetSubfolderAsync ("[Gmail]"); + + var uses = new List { + SpecialFolder.All, + SpecialFolder.Archive, + SpecialFolder.Drafts, + SpecialFolder.Flagged, + SpecialFolder.Important, + SpecialFolder.Junk, + SpecialFolder.Sent, + SpecialFolder.Trash, + + // specifically duplicate some special uses + SpecialFolder.All, + SpecialFolder.Flagged, + + // and add one that is invalid + (SpecialFolder) 15 + }; + + var archive = await gmail.CreateAsync ("Archives", uses); + Assert.That (archive.Attributes, Is.EqualTo (FolderAttributes.HasNoChildren | FolderAttributes.Archive)); + Assert.That (client.GetFolder (SpecialFolder.Archive), Is.EqualTo (archive)); + Assert.That (archive.Id, Is.EqualTo ("25dcfa84-fd65-41c3-abc3-633c8f10923f")); + + try { + await gmail.CreateAsync ("MyImportant", new[] { SpecialFolder.Important }); + Assert.Fail ("Creating the MyImportamnt folder should have thrown an ImapCommandException"); + } catch (ImapCommandException ex) { + Assert.That (ex.Response, Is.EqualTo (ImapCommandResponse.No)); + Assert.That (ex.ResponseText, Is.EqualTo ("An \\Important mailbox already exists")); + } catch (Exception ex) { + Assert.Fail ($"Unexpected exception: {ex}"); + } + + await client.DisconnectAsync (true); + } + } + + static List CreateCopyToCommands () + { + return new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt"), + new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "gmail.authenticate.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-inbox.txt"), + new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt"), + new ImapReplayCommand ("A00000005 SELECT INBOX (CONDSTORE)\r\n", "gmail.select-inbox.txt"), + new ImapReplayCommand ("A00000006 UID SEARCH RETURN (ALL) ALL\r\n", "gmail.search.txt"), + new ImapReplayCommand ("A00000007 LIST \"\" \"Archived Messages\"\r\n", "gmail.list-archived-messages.txt"), + new ImapReplayCommand ("A00000008 UID COPY 1:3,5,7:9,11:14,26:29,31,34,41:43,50 \"Archived Messages\"\r\n", "gmail.uid-copy.txt"), + new ImapReplayCommand ("A00000009 SEARCH UID 1:3,5,7:9,11:14,26:29,31,34,41:43,50\r\n", "gmail.get-indexes.txt"), + new ImapReplayCommand ("A00000010 COPY 1:21 \"Archived Messages\"\r\n", "gmail.uid-copy.txt"), + new ImapReplayCommand ("A00000011 LOGOUT\r\n", "gmail.logout.txt") + }; + } + + [Test] + public void TestCopyTo () + { + var commands = CreateCopyToCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities.HasFlag (ImapCapabilities.UidPlus), Is.True, "Expected UIDPLUS extension"); + + var personal = client.GetFolder (client.PersonalNamespaces[0]); + var inbox = client.Inbox; + + inbox.Open (FolderAccess.ReadWrite); + var uids = inbox.Search (SearchQuery.All); + + var archived = personal.GetSubfolder ("Archived Messages"); + + // Test copying using the UIDPLUS extension + var copied = inbox.CopyTo (uids, archived); + + Assert.That (copied.Destination, Has.Count.EqualTo (copied.Source.Count), "Source and Destination UID counts do not match"); + + // Disable UIDPLUS and try again (to test GetIndexesAsync() and CopyTo(IList, IMailFolder) + client.Capabilities &= ~ImapCapabilities.UidPlus; + copied = inbox.CopyTo (uids, archived); + + Assert.That (copied.Destination, Has.Count.EqualTo (copied.Source.Count), "Source and Destination UID counts do not match"); + + client.Disconnect (true); + } + } + + [Test] + public async Task TestCopyToAsync () + { + var commands = CreateCopyToCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities.HasFlag (ImapCapabilities.UidPlus), Is.True, "Expected UIDPLUS extension"); + + var personal = client.GetFolder (client.PersonalNamespaces[0]); + var inbox = client.Inbox; + + await inbox.OpenAsync (FolderAccess.ReadWrite); + var uids = await inbox.SearchAsync (SearchQuery.All); + + var archived = await personal.GetSubfolderAsync ("Archived Messages"); + + // Test copying using the UIDPLUS extension + var copied = await inbox.CopyToAsync (uids, archived); + + Assert.That (copied.Destination, Has.Count.EqualTo (copied.Source.Count), "Source and Destination UID counts do not match"); + + // Disable UIDPLUS and try again (to test GetIndexesAsync() and CopyTo(IList, IMailFolder) + client.Capabilities &= ~ImapCapabilities.UidPlus; + copied = await inbox.CopyToAsync (uids, archived); + + Assert.That (copied.Destination, Has.Count.EqualTo (copied.Source.Count), "Source and Destination UID counts do not match"); + + await client.DisconnectAsync (true); + } + } + + static List CreateExchangeCopyUidRespCodeWithoutOkCommands () + { + return new List { + new ImapReplayCommand ("", "exchange.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "exchange.capability-preauth.txt"), + new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", ImapReplayCommandResponse.OK), + new ImapReplayCommand ("A00000002 CAPABILITY\r\n", "exchange.capability-postauth.txt"), + new ImapReplayCommand ("A00000003 NAMESPACE\r\n", "gmail.namespace.txt"), + new ImapReplayCommand ("A00000004 LIST \"\" \"INBOX\"\r\n", "common.list-inbox.txt"), + new ImapReplayCommand ("A00000005 SELECT INBOX\r\n", "common.select-inbox.txt"), + new ImapReplayCommand ("A00000006 LIST \"\" Level1\r\n", "gmail.list-level1.txt"), + new ImapReplayCommand ("A00000007 UID MOVE 31 Level1\r\n", "exchange.issue115.txt"), + new ImapReplayCommand ("A00000008 LOGOUT\r\n", "gmail.logout.txt") + }; + } + + [Test] + public void TestExchangeCopyUidRespCodeWithoutOk () + { + var commands = CreateExchangeCopyUidRespCodeWithoutOkCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities.HasFlag (ImapCapabilities.UidPlus), Is.True, "Expected UIDPLUS extension"); + + var personal = client.GetFolder (client.PersonalNamespaces[0]); + var inbox = client.Inbox; + + inbox.Open (FolderAccess.ReadWrite); + + // Test handling of broken Exchange IMAP response: "[COPYUID 55 31 6]" (it should be "* OK [COPYUID 55 31 6]") + var level1 = personal.GetSubfolder ("Level1"); + var uids = new[] { new UniqueId (31) }; + var copied = inbox.MoveTo (uids, level1); + + Assert.That (copied.Destination, Has.Count.EqualTo (copied.Source.Count), "Source and Destination UID counts do not match"); + Assert.That (uids[0], Is.EqualTo (copied.Source[0]), "Source[0]"); + Assert.That (new UniqueId (6), Is.EqualTo (copied.Destination[0]), "Destination[0]"); + + client.Disconnect (true); + } + } + + [Test] + public async Task TestExchangeCopyUidRespCodeWithoutOkAsync () + { + var commands = CreateExchangeCopyUidRespCodeWithoutOkCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities.HasFlag (ImapCapabilities.UidPlus), Is.True, "Expected UIDPLUS extension"); + + var personal = client.GetFolder (client.PersonalNamespaces[0]); + var inbox = client.Inbox; + + await inbox.OpenAsync (FolderAccess.ReadWrite); + + // Test handling of broken Exchange IMAP response: "[COPYUID 55 31 6]" (it should be "* OK [COPYUID 55 31 6]") + var level1 = await personal.GetSubfolderAsync ("Level1"); + var uids = new[] { new UniqueId (31) }; + var copied = await inbox.MoveToAsync (uids, level1); + + Assert.That (copied.Destination, Has.Count.EqualTo (copied.Source.Count), "Source and Destination UID counts do not match"); + Assert.That (uids[0], Is.EqualTo (copied.Source[0]), "Source[0]"); + Assert.That (new UniqueId (6), Is.EqualTo (copied.Destination[0]), "Destination[0]"); + + await client.DisconnectAsync (true); + } + } + + static List CreateMoveToCommands () + { + var commands = new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt"), + new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "gmail.authenticate.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-inbox.txt"), + new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt"), + new ImapReplayCommand ("A00000005 SELECT INBOX (CONDSTORE)\r\n", "gmail.select-inbox.txt"), + new ImapReplayCommand ("A00000006 LIST \"\" \"Archived Messages\"\r\n", "gmail.list-archived-messages.txt"), + new ImapReplayCommand ("A00000007 MOVE 1:21 \"Archived Messages\"\r\n", ImapReplayCommandResponse.OK), + new ImapReplayCommand ("A00000008 COPY 1:21 \"Archived Messages\"\r\n", ImapReplayCommandResponse.OK), + new ImapReplayCommand ("A00000009 STORE 1:21 +FLAGS.SILENT (\\Deleted)\r\n", ImapReplayCommandResponse.OK), + new ImapReplayCommand ("A00000010 LOGOUT\r\n", "gmail.logout.txt") + }; + + return commands; + } + + [Test] + public void TestMoveTo () + { + var commands = CreateMoveToCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities.HasFlag (ImapCapabilities.UidPlus), Is.True, "Expected UIDPLUS extension"); + + var personal = client.GetFolder (client.PersonalNamespaces[0]); + var inbox = client.Inbox; + + inbox.Open (FolderAccess.ReadWrite); + + var indexes = new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 }; + var archived = personal.GetSubfolder ("Archived Messages"); + + inbox.MoveTo (indexes, archived); + + client.Capabilities &= ~ImapCapabilities.Move; + inbox.MoveTo (indexes, archived); + + client.Disconnect (true); + } + } + + [Test] + public async Task TestMoveToAsync () + { + var commands = CreateMoveToCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities.HasFlag (ImapCapabilities.UidPlus), Is.True, "Expected UIDPLUS extension"); + + var personal = client.GetFolder (client.PersonalNamespaces[0]); + var inbox = client.Inbox; + + await inbox.OpenAsync (FolderAccess.ReadWrite); + + var indexes = new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 }; + var archived = await personal.GetSubfolderAsync ("Archived Messages"); + + await inbox.MoveToAsync (indexes, archived); + + client.Capabilities &= ~ImapCapabilities.Move; + await inbox.MoveToAsync (indexes, archived); + + await client.DisconnectAsync (true); + } + } + + static List CreateUidMoveToCommands (bool disableMove) + { + var commands = new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt"), + new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "gmail.authenticate.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-inbox.txt"), + new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt"), + new ImapReplayCommand ("A00000005 SELECT INBOX (CONDSTORE)\r\n", "gmail.select-inbox.txt"), + new ImapReplayCommand ("A00000006 UID SEARCH RETURN (ALL) ALL\r\n", "gmail.search.txt"), + new ImapReplayCommand ("A00000007 LIST \"\" \"Archived Messages\"\r\n", "gmail.list-archived-messages.txt"), + new ImapReplayCommand ("A00000008 UID MOVE 1:3,5,7:9,11:14,26:29,31,34,41:43,50 \"Archived Messages\"\r\n", "gmail.uid-move.txt") + }; + if (disableMove) { + commands.Add (new ImapReplayCommand ("A00000009 UID COPY 1:3,5,7:9,11:14,26:29,31,34,41:43,50 \"Archived Messages\"\r\n", "gmail.uid-copy.txt")); + commands.Add (new ImapReplayCommand ("A00000010 UID STORE 1:3,5,7:9,11:14,26:29,31,34,41:43,50 +FLAGS.SILENT (\\Deleted)\r\n", ImapReplayCommandResponse.OK)); + commands.Add (new ImapReplayCommand ("A00000011 UID EXPUNGE 1:3,5,7:9,11:14,26:29,31,34,41:43,50\r\n", "gmail.uid-expunge.txt")); + commands.Add (new ImapReplayCommand ("A00000012 LOGOUT\r\n", "gmail.logout.txt")); + } else { + commands.Add (new ImapReplayCommand ("A00000009 SEARCH UID 1:3,5,7:9,11:14,26:29,31,34,41:43,50\r\n", "gmail.get-indexes.txt")); + commands.Add (new ImapReplayCommand ("A00000010 MOVE 1:21 \"Archived Messages\"\r\n", "gmail.uid-move.txt")); + commands.Add (new ImapReplayCommand ("A00000011 LOGOUT\r\n", "gmail.logout.txt")); + } + + return commands; + } + + [TestCase (true, TestName = "TestUidMoveToDisableMove")] + [TestCase (false, TestName = "TestUidMoveToDisableUidPlus")] + public void TestUidMoveTo (bool disableMove) + { + var commands = CreateUidMoveToCommands (disableMove); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities.HasFlag (ImapCapabilities.UidPlus), Is.True, "Expected UIDPLUS extension"); + + var personal = client.GetFolder (client.PersonalNamespaces [0]); + var inbox = client.Inbox; + + inbox.Open (FolderAccess.ReadWrite); + var uids = inbox.Search (SearchQuery.All); + + var archived = personal.GetSubfolder ("Archived Messages"); + int changed = 0, expunged = 0; + + inbox.MessageExpunged += (o, e) => { expunged++; Assert.That (e.Index, Is.EqualTo (0), "Expunged event message index"); }; + inbox.CountChanged += (o, e) => { changed++; }; + + // Test copying using the MOVE & UIDPLUS extensions + var moved = inbox.MoveTo (uids, archived); + + Assert.That (moved.Destination, Has.Count.EqualTo (moved.Source.Count), "Source and Destination UID counts do not match"); + Assert.That (expunged, Is.EqualTo (21), "Expunged event"); + Assert.That (changed, Is.EqualTo (1), "CountChanged event"); + + if (disableMove) + client.Capabilities &= ~ImapCapabilities.Move; + else + client.Capabilities &= ~ImapCapabilities.UidPlus; + + expunged = changed = 0; + + moved = inbox.MoveTo (uids, archived); + + Assert.That (moved.Destination, Has.Count.EqualTo (moved.Source.Count), "Source and Destination UID counts do not match"); + Assert.That (expunged, Is.EqualTo (21), "Expunged event"); + Assert.That (changed, Is.EqualTo (1), "CountChanged event"); + + client.Disconnect (true); + } + } + + [TestCase (true, TestName = "TestUidMoveToDisableMoveAsync")] + [TestCase (false, TestName = "TestUidMoveToDisableUidPlusAsync")] + public async Task TestUidMoveToAsync (bool disableMove) + { + var commands = CreateUidMoveToCommands (disableMove); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities.HasFlag (ImapCapabilities.UidPlus), Is.True, "Expected UIDPLUS extension"); + + var personal = client.GetFolder (client.PersonalNamespaces [0]); + var inbox = client.Inbox; + + await inbox.OpenAsync (FolderAccess.ReadWrite); + var uids = await inbox.SearchAsync (SearchQuery.All); + + var archived = await personal.GetSubfolderAsync ("Archived Messages"); + int changed = 0, expunged = 0; + + inbox.MessageExpunged += (o, e) => { expunged++; Assert.That (e.Index, Is.EqualTo (0), "Expunged event message index"); }; + inbox.CountChanged += (o, e) => { changed++; }; + + // Test moving using the MOVE & UIDPLUS extensions + var moved = await inbox.MoveToAsync (uids, archived); + + Assert.That (moved.Destination, Has.Count.EqualTo (moved.Source.Count), "Source and Destination UID counts do not match"); + Assert.That (expunged, Is.EqualTo (21), "Expunged event"); + Assert.That (changed, Is.EqualTo (1), "CountChanged event"); + + if (disableMove) + client.Capabilities &= ~ImapCapabilities.Move; + else + client.Capabilities &= ~ImapCapabilities.UidPlus; + + expunged = changed = 0; + + moved = await inbox.MoveToAsync (uids, archived); + + Assert.That (moved.Destination, Has.Count.EqualTo (moved.Source.Count), "Source and Destination UID counts do not match"); + Assert.That (expunged, Is.EqualTo (21), "Expunged event"); + Assert.That (changed, Is.EqualTo (1), "CountChanged event"); + + await client.DisconnectAsync (true); + } + } + + static List CreateUidExpungeCommands (bool disableUidPlus) + { + var commands = new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt"), + new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "gmail.authenticate.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-inbox.txt"), + new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt"), + new ImapReplayCommand ("A00000005 SELECT INBOX (CONDSTORE)\r\n", "gmail.select-inbox.txt"), + new ImapReplayCommand ("A00000006 UID SEARCH RETURN (ALL) ALL\r\n", "gmail.search.txt"), + new ImapReplayCommand ("A00000007 UID STORE 1:3,5,7:9,11:14,26:29,31,34,41:43,50 +FLAGS.SILENT (\\Deleted)\r\n", ImapReplayCommandResponse.OK) + }; + if (!disableUidPlus) { + commands.Add (new ImapReplayCommand ("A00000008 UID EXPUNGE 1:3\r\n", "gmail.expunge.txt")); + commands.Add (new ImapReplayCommand ("A00000009 LOGOUT\r\n", "gmail.logout.txt")); + } else { + commands.Add (new ImapReplayCommand ("A00000008 UID SEARCH RETURN (ALL) DELETED NOT UID 1:3\r\n", "gmail.search-deleted-not-1-3.txt")); + commands.Add (new ImapReplayCommand ("A00000009 UID STORE 5,7:9,11:14,26:29,31,34,41:43,50 -FLAGS.SILENT (\\Deleted)\r\n", ImapReplayCommandResponse.OK)); + commands.Add (new ImapReplayCommand ("A00000010 EXPUNGE\r\n", "gmail.expunge.txt")); + commands.Add (new ImapReplayCommand ("A00000011 UID STORE 5,7:9,11:14,26:29,31,34,41:43,50 +FLAGS.SILENT (\\Deleted)\r\n", ImapReplayCommandResponse.OK)); + commands.Add (new ImapReplayCommand ("A00000012 LOGOUT\r\n", "gmail.logout.txt")); + } + + return commands; + } + + [TestCase (false, TestName = "TestUidExpunge")] + [TestCase (true, TestName = "TestUidExpungeDisableUidPlus")] + public void TestUidExpunge (bool disableUidPlus) + { + var commands = CreateUidExpungeCommands (disableUidPlus); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + int changed = 0, expunged = 0; + var inbox = client.Inbox; + + inbox.Open (FolderAccess.ReadWrite); + + inbox.MessageExpunged += (o, e) => { expunged++; Assert.That (e.Index, Is.EqualTo (0), "Expunged event message index"); }; + inbox.CountChanged += (o, e) => { changed++; }; + + var uids = inbox.Search (SearchQuery.All); + inbox.AddFlags (uids, MessageFlags.Deleted, true); + + if (disableUidPlus) + client.Capabilities &= ~ImapCapabilities.UidPlus; + + uids = new UniqueIdRange (0, 1, 3); + inbox.Expunge (uids); + + Assert.That (expunged, Is.EqualTo (3), "Unexpected number of Expunged events"); + Assert.That (changed, Is.EqualTo (1), "Unexpected number of CountChanged events"); + Assert.That (inbox, Has.Count.EqualTo (18), "Count"); + + client.Disconnect (true); + } + } + + [TestCase (false, TestName = "TestUidExpungeAsync")] + [TestCase (true, TestName = "TestUidExpungeDisableUidPlusAsync")] + public async Task TestUidExpungeAsync (bool disableUidPlus) + { + var commands = CreateUidExpungeCommands (disableUidPlus); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + int changed = 0, expunged = 0; + var inbox = client.Inbox; + + await inbox.OpenAsync (FolderAccess.ReadWrite); + + inbox.MessageExpunged += (o, e) => { expunged++; Assert.That (e.Index, Is.EqualTo (0), "Expunged event message index"); }; + inbox.CountChanged += (o, e) => { changed++; }; + + var uids = await inbox.SearchAsync (SearchQuery.All); + await inbox.AddFlagsAsync (uids, MessageFlags.Deleted, true); + + if (disableUidPlus) + client.Capabilities &= ~ImapCapabilities.UidPlus; + + uids = new UniqueIdRange (0, 1, 3); + await inbox.ExpungeAsync (uids); + + Assert.That (expunged, Is.EqualTo (3), "Unexpected number of Expunged events"); + Assert.That (changed, Is.EqualTo (1), "Unexpected number of CountChanged events"); + Assert.That (inbox, Has.Count.EqualTo (18), "Count"); + + await client.DisconnectAsync (true); + } + } + + static List CreateExplicitCountChangedCommands () + { + return new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt"), + new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "gmail.authenticate.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-inbox.txt"), + new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt"), + // INBOX has 1 message present in this test + new ImapReplayCommand ("A00000005 EXAMINE INBOX (CONDSTORE)\r\n", "gmail.count.examine.txt"), + // The next response simulates an EXPUNGE notification followed by an explicit EXISTS notification. + new ImapReplayCommand ("A00000006 NOOP\r\n", $"gmail.count-explicit.noop.txt"), + new ImapReplayCommand ("A00000007 LOGOUT\r\n", "gmail.logout.txt") + }; + } + + [Test] + public void TestExplicitCountChanged () + { + var commands = CreateExplicitCountChangedCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + client.Inbox.Open (FolderAccess.ReadOnly); + + int messageExpungedEmitted = 0; + int messageExpungedIndex = -1; + int messageExpungedCount = -1; + int countChangedEmitted = 0; + int countChangedValue = -1; + + client.Inbox.CountChanged += delegate { + countChangedValue = client.Inbox.Count; + countChangedEmitted++; + }; + + client.Inbox.MessageExpunged += delegate (object sender, MessageEventArgs e) { + messageExpungedCount = client.Inbox.Count; + messageExpungedIndex = e.Index; + messageExpungedEmitted++; + }; + + client.NoOp (); + + Assert.That (client.Inbox, Has.Count.EqualTo (1), "Count"); + Assert.That (countChangedEmitted, Is.EqualTo (1), "CountChanged was not emitted the expected number of times"); + Assert.That (countChangedValue, Is.EqualTo (1), "Count was not correct inside of the CountChanged event handler"); + + Assert.That (messageExpungedIndex, Is.EqualTo (0), "The index of the expected message did not match"); + Assert.That (messageExpungedEmitted, Is.EqualTo (1), "MessageExpunged was not emitted the expected number of times"); + Assert.That (messageExpungedCount, Is.EqualTo (0), "Count was not correct inside of the MessageExpunged event handler"); + + client.Disconnect (true); + } + } + + [Test] + public async Task TestExplicitCountChangedAsync () + { + var commands = CreateExplicitCountChangedCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + await client.Inbox.OpenAsync (FolderAccess.ReadOnly); + + int messageExpungedEmitted = 0; + int messageExpungedIndex = -1; + int messageExpungedCount = -1; + int countChangedEmitted = 0; + int countChangedValue = -1; + + client.Inbox.CountChanged += delegate { + countChangedValue = client.Inbox.Count; + countChangedEmitted++; + }; + + client.Inbox.MessageExpunged += delegate (object sender, MessageEventArgs e) { + messageExpungedCount = client.Inbox.Count; + messageExpungedIndex = e.Index; + messageExpungedEmitted++; + }; + + await client.NoOpAsync (); + + Assert.That (client.Inbox, Has.Count.EqualTo (1), "Count"); + Assert.That (countChangedEmitted, Is.EqualTo (1), "CountChanged was not emitted the expected number of times"); + Assert.That (countChangedValue, Is.EqualTo (1), "Count was not correct inside of the CountChanged event handler"); + + Assert.That (messageExpungedIndex, Is.EqualTo (0), "The index of the expected message did not match"); + Assert.That (messageExpungedEmitted, Is.EqualTo (1), "MessageExpunged was not emitted the expected number of times"); + Assert.That (messageExpungedCount, Is.EqualTo (0), "Count was not correct inside of the MessageExpunged event handler"); + + await client.DisconnectAsync (true); + } + } + + static List CreateImplicitCountChangedCommands () + { + return new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt"), + new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "gmail.authenticate.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-inbox.txt"), + new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt"), + // INBOX has 1 message present in this test + new ImapReplayCommand ("A00000005 EXAMINE INBOX (CONDSTORE)\r\n", "gmail.count.examine.txt"), + // The next response simulates an EXPUNGE notification without an explicit EXISTS notification. + new ImapReplayCommand ("A00000006 NOOP\r\n", $"gmail.count-implicit.noop.txt"), + new ImapReplayCommand ("A00000007 LOGOUT\r\n", "gmail.logout.txt") + }; + } + + [Test] + public void TestImplicitCountChanged () + { + var commands = CreateImplicitCountChangedCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + client.Inbox.Open (FolderAccess.ReadOnly); + + int messageExpungedEmitted = 0; + int messageExpungedIndex = -1; + int messageExpungedCount = -1; + int countChangedEmitted = 0; + int countChangedValue = -1; + + client.Inbox.CountChanged += delegate { + countChangedValue = client.Inbox.Count; + countChangedEmitted++; + }; + + client.Inbox.MessageExpunged += delegate (object sender, MessageEventArgs e) { + messageExpungedCount = client.Inbox.Count; + messageExpungedIndex = e.Index; + messageExpungedEmitted++; + }; + + client.NoOp (); + + Assert.That (client.Inbox, Has.Count.EqualTo (0), "Count"); + Assert.That (countChangedEmitted, Is.EqualTo (1), "CountChanged was not emitted the expected number of times"); + Assert.That (countChangedValue, Is.EqualTo (0), "Count was not correct inside of the CountChanged event handler"); + + Assert.That (messageExpungedIndex, Is.EqualTo (0), "The index of the expected message did not match"); + Assert.That (messageExpungedEmitted, Is.EqualTo (1), "MessageExpunged was not emitted the expected number of times"); + Assert.That (messageExpungedCount, Is.EqualTo (0), "Count was not correct inside of the MessageExpunged event handler"); + + client.Disconnect (true); + } + } + + [Test] + public async Task TestImplicitCountChangedAsync () + { + var commands = CreateImplicitCountChangedCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + await client.Inbox.OpenAsync (FolderAccess.ReadOnly); + + int messageExpungedEmitted = 0; + int messageExpungedIndex = -1; + int messageExpungedCount = -1; + int countChangedEmitted = 0; + int countChangedValue = -1; + + client.Inbox.CountChanged += delegate { + countChangedValue = client.Inbox.Count; + countChangedEmitted++; + }; + + client.Inbox.MessageExpunged += delegate (object sender, MessageEventArgs e) { + messageExpungedCount = client.Inbox.Count; + messageExpungedIndex = e.Index; + messageExpungedEmitted++; + }; + + await client.NoOpAsync (); + + Assert.That (client.Inbox, Has.Count.EqualTo (0), "Count"); + Assert.That (countChangedEmitted, Is.EqualTo (1), "CountChanged was not emitted the expected number of times"); + Assert.That (countChangedValue, Is.EqualTo (0), "Count was not correct inside of the CountChanged event handler"); + + Assert.That (messageExpungedIndex, Is.EqualTo (0), "The index of the expected message did not match"); + Assert.That (messageExpungedEmitted, Is.EqualTo (1), "MessageExpunged was not emitted the expected number of times"); + Assert.That (messageExpungedCount, Is.EqualTo (0), "Count was not correct inside of the MessageExpunged event handler"); + + await client.DisconnectAsync (true); + } + } + + static void AssertFolder (IMailFolder folder, string fullName, FolderAttributes attributes, bool subscribed, ulong highestmodseq, int count, int recent, uint uidnext, uint validity, int unread) + { + if (subscribed) + attributes |= FolderAttributes.Subscribed; + + Assert.That (folder.FullName, Is.EqualTo (fullName), "FullName"); + Assert.That (folder.Attributes, Is.EqualTo (attributes), "Attributes"); + Assert.That (folder.IsSubscribed, Is.EqualTo (subscribed), "IsSubscribed"); + Assert.That (folder.HighestModSeq, Is.EqualTo (highestmodseq), "HighestModSeq"); + Assert.That (folder, Has.Count.EqualTo (count), "Count"); + Assert.That (folder.Recent, Is.EqualTo (recent), "Recent"); + Assert.That (folder.Unread, Is.EqualTo (unread), "Unread"); + Assert.That (folder.UidNext.HasValue ? folder.UidNext.Value.Id : (uint)0, Is.EqualTo (uidnext), "UidNext"); + Assert.That (folder.UidValidity, Is.EqualTo (validity), "UidValidity"); + } + + static List CreateGetSubfoldersWithStatusItemsCommands () + { + return new List { + new ImapReplayCommand ("", "gmail.greeting.txt"), + new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "gmail.capability.txt"), + new ImapReplayCommand ("A00000001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "gmail.authenticate.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "gmail.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\" RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-inbox.txt"), + new ImapReplayCommand ("A00000004 XLIST \"\" \"*\"\r\n", "gmail.xlist.txt"), + //new ImapReplayCommand ("A00000005 LIST \"\" \"[Gmail]\"\r\n", "gmail.list-gmail.txt"), + new ImapReplayCommand ("A00000005 LIST (SUBSCRIBED) \"\" \"[Gmail]/%\" RETURN (CHILDREN STATUS (MESSAGES RECENT UIDNEXT UIDVALIDITY UNSEEN HIGHESTMODSEQ))\r\n", "gmail.list-gmail-subfolders.txt"), + new ImapReplayCommand ("A00000006 LIST \"\" \"[Gmail]/%\" RETURN (SUBSCRIBED CHILDREN)\r\n", "gmail.list-gmail-subfolders-no-status.txt"), + new ImapReplayCommand ("A00000007 STATUS \"[Gmail]/All Mail\" (MESSAGES RECENT UIDNEXT UIDVALIDITY UNSEEN HIGHESTMODSEQ)\r\n", "gmail.status-all-mail.txt"), + new ImapReplayCommand ("A00000008 STATUS \"[Gmail]/Drafts\" (MESSAGES RECENT UIDNEXT UIDVALIDITY UNSEEN HIGHESTMODSEQ)\r\n", "gmail.status-drafts.txt"), + new ImapReplayCommand ("A00000009 STATUS \"[Gmail]/Important\" (MESSAGES RECENT UIDNEXT UIDVALIDITY UNSEEN HIGHESTMODSEQ)\r\n", "gmail.status-important.txt"), + new ImapReplayCommand ("A00000010 STATUS \"[Gmail]/Sent Mail\" (MESSAGES RECENT UIDNEXT UIDVALIDITY UNSEEN HIGHESTMODSEQ)\r\n", "gmail.status-all-mail.txt"), + new ImapReplayCommand ("A00000011 STATUS \"[Gmail]/Spam\" (MESSAGES RECENT UIDNEXT UIDVALIDITY UNSEEN HIGHESTMODSEQ)\r\n", "gmail.status-drafts.txt"), + new ImapReplayCommand ("A00000012 STATUS \"[Gmail]/Starred\" (MESSAGES RECENT UIDNEXT UIDVALIDITY UNSEEN HIGHESTMODSEQ)\r\n", "gmail.status-important.txt"), + new ImapReplayCommand ("A00000013 STATUS \"[Gmail]/Trash\" (MESSAGES RECENT UIDNEXT UIDVALIDITY UNSEEN HIGHESTMODSEQ)\r\n", "gmail.status-all-mail.txt"), + new ImapReplayCommand ("A00000014 LOGOUT\r\n", "gmail.logout.txt") + }; + } + + [Test] + public void TestGetSubfoldersWithStatusItems () + { + var commands = CreateGetSubfoldersWithStatusItemsCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.IsConnected, Is.True, "Client failed to connect."); + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + var personal = client.GetFolder (client.PersonalNamespaces[0]); + var gmail = personal.GetSubfolder ("[Gmail]"); + var all = StatusItems.Count | StatusItems.HighestModSeq | StatusItems.Recent | StatusItems.UidNext | StatusItems.UidValidity | StatusItems.Unread; + var folders = gmail.GetSubfolders (all, true); + Assert.That (folders, Has.Count.EqualTo (7), "Unexpected folder count."); + + AssertFolder (folders[0], "[Gmail]/All Mail", FolderAttributes.HasNoChildren | FolderAttributes.All, true, 41234, 67, 0, 1210, 11, 3); + AssertFolder (folders[1], "[Gmail]/Drafts", FolderAttributes.HasNoChildren | FolderAttributes.Drafts, true, 41234, 0, 0, 1, 6, 0); + AssertFolder (folders[2], "[Gmail]/Important", FolderAttributes.HasNoChildren | FolderAttributes.Important, true, 41234, 58, 0, 307, 9, 0); + AssertFolder (folders[3], "[Gmail]/Sent Mail", FolderAttributes.HasNoChildren | FolderAttributes.Sent, true, 41234, 4, 0, 7, 5, 0); + AssertFolder (folders[4], "[Gmail]/Spam", FolderAttributes.HasNoChildren | FolderAttributes.Junk, true, 41234, 0, 0, 1, 3, 0); + AssertFolder (folders[5], "[Gmail]/Starred", FolderAttributes.HasNoChildren | FolderAttributes.Flagged, true, 41234, 1, 0, 7, 4, 0); + AssertFolder (folders[6], "[Gmail]/Trash", FolderAttributes.HasNoChildren | FolderAttributes.Trash, true, 41234, 0, 0, 1143, 2, 0); + + AssertFolder (client.GetFolder (SpecialFolder.All), "[Gmail]/All Mail", FolderAttributes.HasNoChildren | FolderAttributes.All, true, 41234, 67, 0, 1210, 11, 3); + AssertFolder (client.GetFolder (SpecialFolder.Drafts), "[Gmail]/Drafts", FolderAttributes.HasNoChildren | FolderAttributes.Drafts, true, 41234, 0, 0, 1, 6, 0); + AssertFolder (client.GetFolder (SpecialFolder.Important), "[Gmail]/Important", FolderAttributes.HasNoChildren | FolderAttributes.Important, true, 41234, 58, 0, 307, 9, 0); + AssertFolder (client.GetFolder (SpecialFolder.Sent), "[Gmail]/Sent Mail", FolderAttributes.HasNoChildren | FolderAttributes.Sent, true, 41234, 4, 0, 7, 5, 0); + AssertFolder (client.GetFolder (SpecialFolder.Junk), "[Gmail]/Spam", FolderAttributes.HasNoChildren | FolderAttributes.Junk, true, 41234, 0, 0, 1, 3, 0); + AssertFolder (client.GetFolder (SpecialFolder.Flagged), "[Gmail]/Starred", FolderAttributes.HasNoChildren | FolderAttributes.Flagged, true, 41234, 1, 0, 7, 4, 0); + AssertFolder (client.GetFolder (SpecialFolder.Trash), "[Gmail]/Trash", FolderAttributes.HasNoChildren | FolderAttributes.Trash, true, 41234, 0, 0, 1143, 2, 0); + + // Now make the same query but disable LIST-STATUS + client.Capabilities &= ~ImapCapabilities.ListStatus; + folders = gmail.GetSubfolders (all, false); + Assert.That (folders, Has.Count.EqualTo (7), "Unexpected folder count."); + + AssertFolder (folders[0], "[Gmail]/All Mail", FolderAttributes.HasNoChildren | FolderAttributes.All, true, 41234, 67, 0, 1210, 11, 3); + AssertFolder (folders[1], "[Gmail]/Drafts", FolderAttributes.HasNoChildren | FolderAttributes.Drafts, true, 41234, 0, 0, 1, 6, 0); + AssertFolder (folders[2], "[Gmail]/Important", FolderAttributes.HasNoChildren | FolderAttributes.Important | FolderAttributes.Marked, true, 41234, 58, 0, 307, 9, 0); + AssertFolder (folders[3], "[Gmail]/Sent Mail", FolderAttributes.HasNoChildren | FolderAttributes.Sent | FolderAttributes.Unmarked, true, 41234, 4, 0, 7, 5, 0); + AssertFolder (folders[4], "[Gmail]/Spam", FolderAttributes.HasNoChildren | FolderAttributes.Junk, true, 41234, 0, 0, 1, 3, 0); + AssertFolder (folders[5], "[Gmail]/Starred", FolderAttributes.HasNoChildren | FolderAttributes.Flagged, true, 41234, 1, 0, 7, 4, 0); + AssertFolder (folders[6], "[Gmail]/Trash", FolderAttributes.HasNoChildren | FolderAttributes.Trash, true, 41234, 0, 0, 1143, 2, 0); + + AssertFolder (client.GetFolder (SpecialFolder.All), "[Gmail]/All Mail", FolderAttributes.HasNoChildren | FolderAttributes.All, true, 41234, 67, 0, 1210, 11, 3); + AssertFolder (client.GetFolder (SpecialFolder.Drafts), "[Gmail]/Drafts", FolderAttributes.HasNoChildren | FolderAttributes.Drafts, true, 41234, 0, 0, 1, 6, 0); + AssertFolder (client.GetFolder (SpecialFolder.Important), "[Gmail]/Important", FolderAttributes.HasNoChildren | FolderAttributes.Important | FolderAttributes.Marked, true, 41234, 58, 0, 307, 9, 0); + AssertFolder (client.GetFolder (SpecialFolder.Sent), "[Gmail]/Sent Mail", FolderAttributes.HasNoChildren | FolderAttributes.Sent | FolderAttributes.Unmarked, true, 41234, 4, 0, 7, 5, 0); + AssertFolder (client.GetFolder (SpecialFolder.Junk), "[Gmail]/Spam", FolderAttributes.HasNoChildren | FolderAttributes.Junk, true, 41234, 0, 0, 1, 3, 0); + AssertFolder (client.GetFolder (SpecialFolder.Flagged), "[Gmail]/Starred", FolderAttributes.HasNoChildren | FolderAttributes.Flagged, true, 41234, 1, 0, 7, 4, 0); + AssertFolder (client.GetFolder (SpecialFolder.Trash), "[Gmail]/Trash", FolderAttributes.HasNoChildren | FolderAttributes.Trash, true, 41234, 0, 0, 1143, 2, 0); + + client.Disconnect (true); + } + } + + [Test] + public async Task TestGetSubfoldersWithStatusItemsAsync () + { + var commands = CreateGetSubfoldersWithStatusItemsCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + var personal = client.GetFolder (client.PersonalNamespaces[0]); + var gmail = await personal.GetSubfolderAsync ("[Gmail]"); + var all = StatusItems.Count | StatusItems.HighestModSeq | StatusItems.Recent | StatusItems.UidNext | StatusItems.UidValidity | StatusItems.Unread; + var folders = await gmail.GetSubfoldersAsync (all, true); + Assert.That (folders, Has.Count.EqualTo (7), "Unexpected folder count."); + + AssertFolder (folders[0], "[Gmail]/All Mail", FolderAttributes.HasNoChildren | FolderAttributes.All, true, 41234, 67, 0, 1210, 11, 3); + AssertFolder (folders[1], "[Gmail]/Drafts", FolderAttributes.HasNoChildren | FolderAttributes.Drafts, true, 41234, 0, 0, 1, 6, 0); + AssertFolder (folders[2], "[Gmail]/Important", FolderAttributes.HasNoChildren | FolderAttributes.Important, true, 41234, 58, 0, 307, 9, 0); + AssertFolder (folders[3], "[Gmail]/Sent Mail", FolderAttributes.HasNoChildren | FolderAttributes.Sent, true, 41234, 4, 0, 7, 5, 0); + AssertFolder (folders[4], "[Gmail]/Spam", FolderAttributes.HasNoChildren | FolderAttributes.Junk, true, 41234, 0, 0, 1, 3, 0); + AssertFolder (folders[5], "[Gmail]/Starred", FolderAttributes.HasNoChildren | FolderAttributes.Flagged, true, 41234, 1, 0, 7, 4, 0); + AssertFolder (folders[6], "[Gmail]/Trash", FolderAttributes.HasNoChildren | FolderAttributes.Trash, true, 41234, 0, 0, 1143, 2, 0); + + AssertFolder (client.GetFolder (SpecialFolder.All), "[Gmail]/All Mail", FolderAttributes.HasNoChildren | FolderAttributes.All, true, 41234, 67, 0, 1210, 11, 3); + AssertFolder (client.GetFolder (SpecialFolder.Drafts), "[Gmail]/Drafts", FolderAttributes.HasNoChildren | FolderAttributes.Drafts, true, 41234, 0, 0, 1, 6, 0); + AssertFolder (client.GetFolder (SpecialFolder.Important), "[Gmail]/Important", FolderAttributes.HasNoChildren | FolderAttributes.Important, true, 41234, 58, 0, 307, 9, 0); + AssertFolder (client.GetFolder (SpecialFolder.Sent), "[Gmail]/Sent Mail", FolderAttributes.HasNoChildren | FolderAttributes.Sent, true, 41234, 4, 0, 7, 5, 0); + AssertFolder (client.GetFolder (SpecialFolder.Junk), "[Gmail]/Spam", FolderAttributes.HasNoChildren | FolderAttributes.Junk, true, 41234, 0, 0, 1, 3, 0); + AssertFolder (client.GetFolder (SpecialFolder.Flagged), "[Gmail]/Starred", FolderAttributes.HasNoChildren | FolderAttributes.Flagged, true, 41234, 1, 0, 7, 4, 0); + AssertFolder (client.GetFolder (SpecialFolder.Trash), "[Gmail]/Trash", FolderAttributes.HasNoChildren | FolderAttributes.Trash, true, 41234, 0, 0, 1143, 2, 0); + + // Now make the same query but disable LIST-STATUS + client.Capabilities &= ~ImapCapabilities.ListStatus; + folders = await gmail.GetSubfoldersAsync (all, false); + Assert.That (folders, Has.Count.EqualTo (7), "Unexpected folder count."); + + AssertFolder (folders[0], "[Gmail]/All Mail", FolderAttributes.HasNoChildren | FolderAttributes.All, true, 41234, 67, 0, 1210, 11, 3); + AssertFolder (folders[1], "[Gmail]/Drafts", FolderAttributes.HasNoChildren | FolderAttributes.Drafts, true, 41234, 0, 0, 1, 6, 0); + AssertFolder (folders[2], "[Gmail]/Important", FolderAttributes.HasNoChildren | FolderAttributes.Important | FolderAttributes.Marked, true, 41234, 58, 0, 307, 9, 0); + AssertFolder (folders[3], "[Gmail]/Sent Mail", FolderAttributes.HasNoChildren | FolderAttributes.Sent | FolderAttributes.Unmarked, true, 41234, 4, 0, 7, 5, 0); + AssertFolder (folders[4], "[Gmail]/Spam", FolderAttributes.HasNoChildren | FolderAttributes.Junk, true, 41234, 0, 0, 1, 3, 0); + AssertFolder (folders[5], "[Gmail]/Starred", FolderAttributes.HasNoChildren | FolderAttributes.Flagged, true, 41234, 1, 0, 7, 4, 0); + AssertFolder (folders[6], "[Gmail]/Trash", FolderAttributes.HasNoChildren | FolderAttributes.Trash, true, 41234, 0, 0, 1143, 2, 0); + + AssertFolder (client.GetFolder (SpecialFolder.All), "[Gmail]/All Mail", FolderAttributes.HasNoChildren | FolderAttributes.All, true, 41234, 67, 0, 1210, 11, 3); + AssertFolder (client.GetFolder (SpecialFolder.Drafts), "[Gmail]/Drafts", FolderAttributes.HasNoChildren | FolderAttributes.Drafts, true, 41234, 0, 0, 1, 6, 0); + AssertFolder (client.GetFolder (SpecialFolder.Important), "[Gmail]/Important", FolderAttributes.HasNoChildren | FolderAttributes.Important | FolderAttributes.Marked, true, 41234, 58, 0, 307, 9, 0); + AssertFolder (client.GetFolder (SpecialFolder.Sent), "[Gmail]/Sent Mail", FolderAttributes.HasNoChildren | FolderAttributes.Sent | FolderAttributes.Unmarked, true, 41234, 4, 0, 7, 5, 0); + AssertFolder (client.GetFolder (SpecialFolder.Junk), "[Gmail]/Spam", FolderAttributes.HasNoChildren | FolderAttributes.Junk, true, 41234, 0, 0, 1, 3, 0); + AssertFolder (client.GetFolder (SpecialFolder.Flagged), "[Gmail]/Starred", FolderAttributes.HasNoChildren | FolderAttributes.Flagged, true, 41234, 1, 0, 7, 4, 0); + AssertFolder (client.GetFolder (SpecialFolder.Trash), "[Gmail]/Trash", FolderAttributes.HasNoChildren | FolderAttributes.Trash, true, 41234, 0, 0, 1143, 2, 0); + + await client.DisconnectAsync (true); + } + } + } +} diff --git a/UnitTests/Net/Imap/ImapImplementationTests.cs b/UnitTests/Net/Imap/ImapImplementationTests.cs new file mode 100644 index 0000000000..b1295e156d --- /dev/null +++ b/UnitTests/Net/Imap/ImapImplementationTests.cs @@ -0,0 +1,72 @@ +// +// ImapImplementationTests.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using MailKit.Net.Imap; + +namespace UnitTests.Net.Imap { + [TestFixture] + public class ImapImplementationTests + { + [Test] + public void TestImapImplementationProperties () + { + var impl = new ImapImplementation (); + + impl.Address = "50 Church St."; + Assert.That (impl.Address, Is.EqualTo ("50 Church St."), "Address"); + + impl.Arguments = "-p -q"; + Assert.That (impl.Arguments, Is.EqualTo ("-p -q"), "Arguments"); + + impl.Command = "mono ./imap.exe"; + Assert.That (impl.Command, Is.EqualTo ("mono ./imap.exe"), "Command"); + + impl.Environment = "MONO_GC=sgen"; + Assert.That (impl.Environment, Is.EqualTo ("MONO_GC=sgen"), "Environment"); + + impl.Name = "MailKit"; + Assert.That (impl.Name, Is.EqualTo ("MailKit"), "Name"); + + impl.OS = "Windows"; + Assert.That (impl.OS, Is.EqualTo ("Windows"), "OS"); + + impl.OSVersion = "6.1"; + Assert.That (impl.OSVersion, Is.EqualTo ("6.1"), "OSVersion"); + + impl.ReleaseDate = "${Date}"; + Assert.That (impl.ReleaseDate, Is.EqualTo ("${Date}"), "ReleaseDate"); + + impl.SupportUrl = "https://github.com/jstedfast/MailKit"; + Assert.That (impl.SupportUrl, Is.EqualTo ("https://github.com/jstedfast/MailKit"), "SupportUrl"); + + impl.Vendor = "Microsoft"; + Assert.That (impl.Vendor, Is.EqualTo ("Microsoft"), "Vendor"); + + impl.Version = "2.0.7"; + Assert.That (impl.Version, Is.EqualTo ("2.0.7"), "Version"); + } + } +} diff --git a/UnitTests/Net/Imap/ImapReplayStream.cs b/UnitTests/Net/Imap/ImapReplayStream.cs index f54c007013..853bd0d732 100644 --- a/UnitTests/Net/Imap/ImapReplayStream.cs +++ b/UnitTests/Net/Imap/ImapReplayStream.cs @@ -1,9 +1,9 @@ -// +// // ImapReplayStream.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -24,59 +24,287 @@ // THE SOFTWARE. // -using System; -using System.IO; using System.Text; -using System.Collections.Generic; - -using NUnit.Framework; using MimeKit.IO; using MimeKit.IO.Filters; +using MailKit; + namespace UnitTests.Net.Imap { enum ImapReplayCommandResponse { OK, NO, - BAD + BAD, + Plus + } + + class ImapReplayFilter : MimeFilterBase + { + readonly byte[] variable; + readonly byte[] value; + + public ImapReplayFilter (string variable, string value) + { + this.variable = Encoding.ASCII.GetBytes (variable); + this.value = Encoding.ASCII.GetBytes (value); + } + + protected override byte[] Filter (byte[] input, int startIndex, int length, out int outputIndex, out int outputLength, bool flush) + { + int endIndex = startIndex + length; + int copyIndex = startIndex; + int copyLength = 0; + + for (int index = startIndex; index < endIndex - variable.Length; index++) { + var matched = true; + + for (int i = 0; i < variable.Length; i++) { + if (input[index + i] != variable[i]) { + matched = false; + break; + } + } + + if (!matched) + continue; + + int n = index - copyIndex; + + EnsureOutputSize (copyLength + n + value.Length, true); + Buffer.BlockCopy (input, copyIndex, OutputBuffer, copyLength, n); + index += variable.Length; + copyIndex = index; + copyLength += n; + + Buffer.BlockCopy (value, 0, OutputBuffer, copyLength, value.Length); + copyLength += value.Length; + } + + if (flush) { + if (copyLength == 0) { + outputIndex = startIndex; + outputLength = length; + + return input; + } + + int n = endIndex - copyIndex; + + EnsureOutputSize (copyLength + n, true); + Buffer.BlockCopy (input, copyIndex, OutputBuffer, copyLength, n); + copyLength += n; + + outputLength = copyLength; + outputIndex = 0; + + return OutputBuffer; + } else { + int n = Math.Min (variable.Length, length); + + SaveRemainingInput (input, endIndex - n, n); + + if (copyLength == 0) { + outputLength = length - n; + outputIndex = startIndex; + + return input; + } + + endIndex -= n; + + if (endIndex > copyIndex) { + n = endIndex - copyIndex; + EnsureOutputSize (copyLength + n, true); + Buffer.BlockCopy (input, copyIndex, OutputBuffer, copyLength, n); + copyLength += n; + } + + outputLength = copyLength; + outputIndex = 0; + + return OutputBuffer; + } + } + + public override void Reset () + { + base.Reset (); + } } class ImapReplayCommand { + static readonly Encoding Latin1 = Encoding.GetEncoding (28591); + + public Encoding Encoding { get; private set; } + public byte[] CommandBuffer { get; private set; } public string Command { get; private set; } public byte[] Response { get; private set; } + public bool Compressed { get; private set; } - public ImapReplayCommand (string command, byte[] response) + public ImapReplayCommand (string command, byte[] response, bool compressed = false) : this (Latin1, command, response, compressed) { - Command = command; + } + + public ImapReplayCommand (Encoding encoding, string command, byte[] response, bool compressed = false) + { + CommandBuffer = encoding.GetBytes (command); + Compressed = compressed; Response = response; + Encoding = encoding; + Command = command; + + if (compressed) { + using (var memory = new MemoryStream ()) { + using (var compress = new CompressedStream (memory)) { + compress.Write (CommandBuffer, 0, CommandBuffer.Length); + compress.Flush (); + + CommandBuffer = memory.ToArray (); + } + } + + using (var memory = new MemoryStream ()) { + using (Stream compress = new CompressedStream (memory)) { + compress.Write (response, 0, response.Length); + compress.Flush (); + + Response = memory.ToArray (); + } + } + } } - public ImapReplayCommand (string command, string resource) + public ImapReplayCommand (string command, string resource, bool compressed = false) : this (Latin1, command, resource, compressed) { + } + + public ImapReplayCommand (Encoding encoding, string command, string resource, bool compressed = false) + { + string tag = null; + + CommandBuffer = encoding.GetBytes (command); + Compressed = compressed; + Encoding = encoding; Command = command; + if (command.StartsWith ("A00000", StringComparison.Ordinal)) + tag = command.Substring (0, 9); + using (var stream = GetType ().Assembly.GetManifestResourceStream ("UnitTests.Net.Imap.Resources." + resource)) { - var memory = new MemoryBlockStream (); + using (var memory = new MemoryBlockStream ()) { + using (Stream compress = new CompressedStream (memory)) { + using (var filtered = new FilteredStream (compressed ? compress : memory)) { + if (tag != null) + filtered.Add (new ImapReplayFilter ("A########", tag)); + + filtered.Add (new Unix2DosFilter ()); + stream.CopyTo (filtered, 4096); + filtered.Flush (); + } + + Response = memory.ToArray (); + } + } + } - using (var filtered = new FilteredStream (memory)) { - filtered.Add (new Unix2DosFilter ()); - stream.CopyTo (filtered, 4096); + if (compressed) { + using (var memory = new MemoryStream ()) { + using (var compress = new CompressedStream (memory)) { + compress.Write (CommandBuffer, 0, CommandBuffer.Length); + compress.Flush (); + + CommandBuffer = memory.ToArray (); + } } + } + } + + public ImapReplayCommand (string tag, string command, string resource, bool compressed = false) : this (Latin1, tag, command, resource, compressed) + { + } + + public ImapReplayCommand (Encoding encoding, string tag, string command, string resource, bool compressed = false) + { + CommandBuffer = encoding.GetBytes (command); + Compressed = compressed; + Encoding = encoding; + Command = command; - Response = memory.ToArray (); + using (var stream = GetType ().Assembly.GetManifestResourceStream ("UnitTests.Net.Imap.Resources." + resource)) { + using (var memory = new MemoryBlockStream ()) { + using (Stream compress = new CompressedStream (memory)) { + using (var filtered = new FilteredStream (compressed ? compress : memory)) { + filtered.Add (new ImapReplayFilter ("A########", tag)); + filtered.Add (new Unix2DosFilter ()); + stream.CopyTo (filtered, 4096); + filtered.Flush (); + } + + Response = memory.ToArray (); + } + } + } + + if (compressed) { + using (var memory = new MemoryStream ()) { + using (var compress = new CompressedStream (memory)) { + compress.Write (CommandBuffer, 0, CommandBuffer.Length); + compress.Flush (); + + CommandBuffer = memory.ToArray (); + } + } } } - public ImapReplayCommand (string command, ImapReplayCommandResponse response) + public ImapReplayCommand (string command, ImapReplayCommandResponse response, bool compressed = false) : this (Latin1, command, response, compressed) { - var tokens = command.Split (' '); - var cmd = (tokens[1] == "UID" ? tokens[2] : tokens[1]).TrimEnd (); - var tag = tokens[0]; + } - var text = string.Format ("{0} {1} {2} completed\r\n", tag, response, cmd); - Response = Encoding.ASCII.GetBytes (text); + public ImapReplayCommand (Encoding encoding, string command, ImapReplayCommandResponse response, bool compressed = false) + { + CommandBuffer = encoding.GetBytes (command); + Compressed = compressed; + Encoding = encoding; Command = command; + + string text; + + if (response == ImapReplayCommandResponse.Plus) { + text = "+\r\n"; + } else { + var tokens = command.Split (' '); + var cmd = (tokens [1] == "UID" ? tokens [2] : tokens [1]).TrimEnd (); + var tag = tokens [0]; + + text = string.Format ("{0} {1} {2} {3}\r\n", tag, response, cmd, response == ImapReplayCommandResponse.OK ? "completed" : "failed"); + } + + if (compressed) { + using (var memory = new MemoryStream ()) { + using (var compress = new CompressedStream (memory)) { + var buffer = encoding.GetBytes (text); + + compress.Write (buffer, 0, buffer.Length); + compress.Flush (); + + Response = memory.ToArray (); + } + } + + using (var memory = new MemoryStream ()) { + using (var compress = new CompressedStream (memory)) { + compress.Write (CommandBuffer, 0, CommandBuffer.Length); + compress.Flush (); + + CommandBuffer = memory.ToArray (); + } + } + } else { + Response = encoding.GetBytes (text); + } } } @@ -87,22 +315,25 @@ enum ImapReplayState { class ImapReplayStream : Stream { - static readonly Encoding Latin1 = Encoding.GetEncoding (28591); readonly MemoryStream sent = new MemoryStream (); readonly IList commands; readonly bool testUnixFormat; + readonly bool asyncIO; ImapReplayState state; int timeout = 100000; Stream stream; bool disposed; + bool isAsync; + bool done; int index; - public ImapReplayStream (IList commands, bool testUnixFormat) + public ImapReplayStream (IList commands, bool asyncIO, bool testUnixFormat = false) { stream = GetResponseStream (commands[0]); state = ImapReplayState.SendResponse; this.testUnixFormat = testUnixFormat; this.commands = commands; + this.asyncIO = asyncIO; } void CheckDisposed () @@ -152,12 +383,21 @@ public override int Read (byte[] buffer, int offset, int count) { CheckDisposed (); + if (asyncIO) { + Assert.That (isAsync, Is.True, "Trying to Read in an async unit test."); + } else { + Assert.That (isAsync, Is.False, "Trying to ReadAsync in a non-async unit test."); + } + if (state != ImapReplayState.SendResponse) { - var command = Latin1.GetString (sent.GetBuffer (), 0, (int) sent.Length); + if (index >= commands.Count) + return 0; + + var command = GetSentCommand (); - Assert.AreEqual (ImapReplayState.SendResponse, state, "Trying to read before command received. Sent so far: {0}", command); + Assert.That (state, Is.EqualTo (ImapReplayState.SendResponse), $"Trying to read before command received. Sent so far: {command}"); } - Assert.IsNotNull (stream, "Trying to read when no data available."); + Assert.That (stream, Is.Not.Null, "Trying to read when no data available."); int nread = stream.Read (buffer, offset, count); @@ -169,11 +409,22 @@ public override int Read (byte[] buffer, int offset, int count) return nread; } + public override Task ReadAsync (byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + isAsync = true; + + try { + return Task.FromResult (Read (buffer, offset, count)); + } finally { + isAsync = false; + } + } + Stream GetResponseStream (ImapReplayCommand command) { MemoryStream memory; - if (testUnixFormat) { + if (testUnixFormat && !command.Compressed) { memory = new MemoryStream (); using (var filtered = new FilteredStream (memory)) { @@ -190,21 +441,48 @@ Stream GetResponseStream (ImapReplayCommand command) return memory; } + string GetSentCommand () + { + if (!commands[index].Compressed) + return commands[index].Encoding.GetString (sent.GetBuffer (), 0, (int) sent.Length); + + using (var memory = new MemoryStream (sent.GetBuffer (), 0, (int) sent.Length)) { + using (var compressed = new CompressedStream (memory)) { + using (var decompressed = new MemoryStream ()) { + compressed.CopyTo (decompressed, 4096); + + return commands[index].Encoding.GetString (decompressed.GetBuffer (), 0, (int) decompressed.Length); + } + } + } + } + public override void Write (byte[] buffer, int offset, int count) { CheckDisposed (); - Assert.AreEqual (ImapReplayState.WaitForCommand, state, "Trying to write when a command has already been given."); + if (asyncIO) { + if (count != 6 || Encoding.ASCII.GetString (buffer, offset, count) != "DONE\r\n") + Assert.That (isAsync, Is.True, "Trying to Write in an async unit test."); + else + done = true; + } else { + if (count != 6 || Encoding.ASCII.GetString (buffer, offset, count) != "DONE\r\n") + Assert.That (isAsync, Is.False, "Trying to WriteAsync in a non-async unit test."); + else + done = true; + } + + Assert.That (state, Is.EqualTo (ImapReplayState.WaitForCommand), "Trying to write when a command has already been given."); sent.Write (buffer, offset, count); - if (sent.Length >= commands[index].Command.Length) { - var command = Latin1.GetString (sent.GetBuffer (), 0, (int) sent.Length); + if (sent.Length >= commands[index].CommandBuffer.Length) { + var command = GetSentCommand (); - Assert.AreEqual (commands[index].Command, command, "Commands did not match."); + Assert.That (command, Is.EqualTo (commands[index].Command), "Commands did not match."); - if (stream != null) - stream.Dispose (); + stream?.Dispose (); stream = GetResponseStream (commands[index]); state = ImapReplayState.SendResponse; @@ -212,9 +490,34 @@ public override void Write (byte[] buffer, int offset, int count) } } + public override Task WriteAsync (byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + isAsync = true; + + try { + Write (buffer, offset, count); + return Task.FromResult (true); + } finally { + isAsync = false; + } + } + public override void Flush () { CheckDisposed (); + + Assert.That (asyncIO && !done, Is.False, "Trying to Flush in an async unit test."); + done = false; + } + + public override Task FlushAsync (CancellationToken cancellationToken) + { + CheckDisposed (); + + Assert.That (asyncIO || done, Is.True, "Trying to FlushAsync in a non-async unit test."); + done = false; + + return Task.FromResult (true); } public override long Seek (long offset, SeekOrigin origin) @@ -231,8 +534,7 @@ public override void SetLength (long value) protected override void Dispose (bool disposing) { - if (stream != null) - stream.Dispose (); + stream?.Dispose (); base.Dispose (disposing); disposed = true; diff --git a/UnitTests/Net/Imap/ImapSearchQueryOptimizerTests.cs b/UnitTests/Net/Imap/ImapSearchQueryOptimizerTests.cs new file mode 100644 index 0000000000..4e9c109148 --- /dev/null +++ b/UnitTests/Net/Imap/ImapSearchQueryOptimizerTests.cs @@ -0,0 +1,104 @@ +// +// ImapSearchQueryOptimizerTests.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using MailKit; +using MailKit.Search; +using MailKit.Net.Imap; + +namespace UnitTests.Net.Imap { + [TestFixture] + public class ImapSearchQueryOptimizerTests + { + readonly ImapSearchQueryOptimizer optimizer = new ImapSearchQueryOptimizer (); + + [Test] + public void TestReduceAnd () + { + var query = optimizer.Reduce (SearchQuery.And (SearchQuery.All, SearchQuery.Answered)); + + Assert.That (query, Is.EqualTo (SearchQuery.Answered)); + + query = optimizer.Reduce (SearchQuery.And (SearchQuery.Answered, SearchQuery.All)); + + Assert.That (query, Is.EqualTo (SearchQuery.Answered)); + } + + [Test] + public void TestReduceOr () + { + var query = optimizer.Reduce (SearchQuery.Or (SearchQuery.All, SearchQuery.Answered)); + + Assert.That (query, Is.EqualTo (SearchQuery.All)); + + query = optimizer.Reduce (SearchQuery.Or (SearchQuery.Answered, SearchQuery.All)); + + Assert.That (query, Is.EqualTo (SearchQuery.All)); + } + + [Test] + public void TestReduceNotFlags () + { + foreach (MessageFlags flag in Enum.GetValues (typeof (MessageFlags))) { + if (flag == MessageFlags.None || flag == MessageFlags.UserDefined) + continue; + + var query = SearchQuery.Not (SearchQuery.HasFlags (flag)); + var optimized = optimizer.Reduce (query); + + Assert.That (optimized.Term.ToString (), Is.EqualTo ("Not" + flag.ToString ()), $"NOT ({flag})"); + + query = SearchQuery.Not (SearchQuery.NotFlags (flag)); + optimized = optimizer.Reduce (query); + + Assert.That (optimized.Term.ToString (), Is.EqualTo (flag.ToString ()), $"NOT ({query.Operand.Term})"); + + query = SearchQuery.Not (SearchQuery.Not (SearchQuery.HasFlags (flag))); + optimized = optimizer.Reduce (query); + + Assert.That (optimized.Term.ToString (), Is.EqualTo (flag.ToString ()), $"NOT (NOT ({flag}))"); + } + } + + [Test] + public void TestReduceNotFlag () + { + var query = SearchQuery.Not (SearchQuery.HasKeyword ("custom")); + var optimized = optimizer.Reduce (query); + + Assert.That (optimized.Term, Is.EqualTo (SearchTerm.NotKeyword), "NOT KEYWORD"); + + query = SearchQuery.Not (SearchQuery.NotKeyword ("custom")); + optimized = optimizer.Reduce (query); + + Assert.That (optimized.Term, Is.EqualTo (SearchTerm.Keyword), $"NOT NOTKEYWORD"); + + query = SearchQuery.Not (SearchQuery.Not (SearchQuery.HasKeyword ("custom"))); + optimized = optimizer.Reduce (query); + + Assert.That (optimized.Term, Is.EqualTo (SearchTerm.Keyword), "NOT NOT KEYWORD"); + } + } +} diff --git a/UnitTests/Net/Imap/ImapStreamTests.cs b/UnitTests/Net/Imap/ImapStreamTests.cs new file mode 100644 index 0000000000..52a0fe2d02 --- /dev/null +++ b/UnitTests/Net/Imap/ImapStreamTests.cs @@ -0,0 +1,507 @@ +// +// ImapStreamTests.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2026 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System.Text; +using System.Security.Cryptography; + +using MailKit; +using MailKit.Net.Imap; + +namespace UnitTests.Net.Imap { + [TestFixture] + public class ImapStreamTests + { + [Test] + public void TestCanReadWriteSeek () + { + using (var stream = new ImapStream (new DummyNetworkStream (), new NullProtocolLogger ())) { + Assert.That (stream.CanRead, Is.True); + Assert.That (stream.CanWrite, Is.True); + Assert.That (stream.CanSeek, Is.False); + Assert.That (stream.CanTimeout, Is.True); + } + } + + [Test] + public void TestGetSetTimeouts () + { + using (var stream = new ImapStream (new DummyNetworkStream (), new NullProtocolLogger ())) { + stream.ReadTimeout = 5; + Assert.That (stream.ReadTimeout, Is.EqualTo (5), "ReadTimeout"); + + stream.WriteTimeout = 7; + Assert.That (stream.WriteTimeout, Is.EqualTo (7), "WriteTimeout"); + } + } + + [Test] + public void TestRead () + { + using (var stream = new ImapStream (new DummyNetworkStream (), new NullProtocolLogger ())) { + var data = Encoding.ASCII.GetBytes ("This is some random text...\r\n"); + var buffer = new byte[32]; + int n; + + Assert.Throws (() => stream.Read (null, 0, buffer.Length)); + Assert.Throws (() => stream.Read (buffer, -1, buffer.Length)); + Assert.Throws (() => stream.Read (buffer, 0, -1)); + + stream.Stream.Write (data, 0, data.Length); + stream.Stream.Position = 0; + + stream.LiteralLength = data.Length; + + stream.Mode = ImapStreamMode.Token; + n = stream.Read (buffer, 0, buffer.Length); + Assert.That (n, Is.EqualTo (0), "ImapStreamMode.Token"); + + stream.Mode = ImapStreamMode.Literal; + n = stream.Read (buffer, 0, buffer.Length); + Assert.That (n, Is.EqualTo (data.Length), "ImapStreamMode.Literal"); + Assert.That (Encoding.ASCII.GetString (buffer, 0, n), Is.EqualTo ("This is some random text...\r\n"), "Read"); + } + } + + [Test] + public async Task TestReadAsync () + { + using (var stream = new ImapStream (new DummyNetworkStream (), new NullProtocolLogger ())) { + var data = Encoding.ASCII.GetBytes ("This is some random text...\r\n"); + var buffer = new byte[32]; + int n; + + Assert.ThrowsAsync (async () => await stream.ReadAsync (null, 0, buffer.Length)); + Assert.ThrowsAsync (async () => await stream.ReadAsync (buffer, -1, buffer.Length)); + Assert.ThrowsAsync (async () => await stream.ReadAsync (buffer, 0, -1)); + + stream.Stream.Write (data, 0, data.Length); + stream.Stream.Position = 0; + + stream.LiteralLength = data.Length; + + stream.Mode = ImapStreamMode.Token; + n = await stream.ReadAsync (buffer, 0, buffer.Length); + Assert.That (n, Is.EqualTo (0), "ImapStreamMode.Token"); + + stream.Mode = ImapStreamMode.Literal; + n = await stream.ReadAsync (buffer, 0, buffer.Length); + Assert.That (n, Is.EqualTo (data.Length), "Read"); + Assert.That (Encoding.ASCII.GetString (buffer, 0, n), Is.EqualTo ("This is some random text...\r\n"), "Read"); + } + } + + [Test] + public void TestReadLine () + { + var line1 = "This is a really long line..." + new string ('.', 4096) + "\r\n"; + var line2 = "And this is another line...\r\n"; + + using (var stream = new ImapStream (new DummyNetworkStream (), new NullProtocolLogger ())) { + var data = Encoding.ASCII.GetBytes (line1 + line2); + + stream.Stream.Write (data, 0, data.Length); + stream.Stream.Position = 0; + + using (var builder = new ByteArrayBuilder (64)) { + while (!stream.ReadLine (builder, CancellationToken.None)) + ; + + var text = builder.ToString (); + + Assert.That (text, Is.EqualTo (line1), "Line1"); + } + + using (var builder = new ByteArrayBuilder (64)) { + while (!stream.ReadLine (builder, CancellationToken.None)) + ; + + var text = builder.ToString (); + + Assert.That (text, Is.EqualTo (line2), "Line2"); + } + } + } + + [Test] + public async Task TestReadLineAsync () + { + var line1 = "This is a really long line..." + new string ('.', 4096) + "\r\n"; + var line2 = "And this is another line...\r\n"; + + using (var stream = new ImapStream (new DummyNetworkStream (), new NullProtocolLogger ())) { + var data = Encoding.ASCII.GetBytes (line1 + line2); + + stream.Stream.Write (data, 0, data.Length); + stream.Stream.Position = 0; + + using (var builder = new ByteArrayBuilder (64)) { + while (!await stream.ReadLineAsync (builder, CancellationToken.None)) + ; + + var text = builder.ToString (); + + Assert.That (text, Is.EqualTo (line1), "Line1"); + } + + using (var builder = new ByteArrayBuilder (64)) { + while (!await stream.ReadLineAsync (builder, CancellationToken.None)) + ; + + var text = builder.ToString (); + + Assert.That (text, Is.EqualTo (line2), "Line2"); + } + } + } + + [Test] + public void TestReadToken () + { + using (var stream = new ImapStream (new DummyNetworkStream (), new NullProtocolLogger ())) { + var data = Encoding.ASCII.GetBytes ("* atom (\\flag \"qstring\" NIL Nil nil) [] \r\n"); + + stream.Stream.Write (data, 0, data.Length); + stream.Stream.Position = 0; + + Assert.Throws (() => stream.UngetToken (null)); + + var token = stream.ReadToken (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Asterisk)); + Assert.That (token.ToString (), Is.EqualTo ("'*'")); + + token = stream.ReadToken (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Atom)); + Assert.That (token.ToString (), Is.EqualTo ("atom")); + + token = stream.ReadToken (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.OpenParen)); + Assert.That (token.ToString (), Is.EqualTo ("'('")); + + token = stream.ReadToken (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Flag)); + Assert.That (token.ToString (), Is.EqualTo ("\\flag")); + + token = stream.ReadToken (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.QString)); + Assert.That (token.ToString (), Is.EqualTo ("\"qstring\"")); + + token = stream.ReadToken (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Nil)); + Assert.That (token.ToString (), Is.EqualTo ("NIL")); + + token = stream.ReadToken (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Nil)); + Assert.That (token.ToString (), Is.EqualTo ("Nil")); + + token = stream.ReadToken (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Nil)); + Assert.That (token.ToString (), Is.EqualTo ("nil")); + + token = stream.ReadToken (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.CloseParen)); + Assert.That (token.ToString (), Is.EqualTo ("')'")); + + token = stream.ReadToken (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.OpenBracket)); + Assert.That (token.ToString (), Is.EqualTo ("'['")); + + token = stream.ReadToken (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.CloseBracket)); + Assert.That (token.ToString (), Is.EqualTo ("']'")); + + token = stream.ReadToken (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln)); + Assert.That (token.ToString (), Is.EqualTo ("'\\n'")); + + stream.UngetToken (token); + token = stream.ReadToken (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln)); + Assert.That (token.ToString (), Is.EqualTo ("'\\n'")); + } + } + + [Test] + public async Task TestReadTokenAsync () + { + using (var stream = new ImapStream (new DummyNetworkStream (), new NullProtocolLogger ())) { + var data = Encoding.ASCII.GetBytes ("* atom (\\flag \"qstring\" NIL Nil nil) [] \r\n"); + + stream.Stream.Write (data, 0, data.Length); + stream.Stream.Position = 0; + + Assert.Throws (() => stream.UngetToken (null)); + + var token = await stream.ReadTokenAsync (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Asterisk)); + Assert.That (token.ToString (), Is.EqualTo ("'*'")); + + token = await stream.ReadTokenAsync (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Atom)); + Assert.That (token.ToString (), Is.EqualTo ("atom")); + + token = await stream.ReadTokenAsync (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.OpenParen)); + Assert.That (token.ToString (), Is.EqualTo ("'('")); + + token = await stream.ReadTokenAsync (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Flag)); + Assert.That (token.ToString (), Is.EqualTo ("\\flag")); + + token = await stream.ReadTokenAsync (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.QString)); + Assert.That (token.ToString (), Is.EqualTo ("\"qstring\"")); + + token = await stream.ReadTokenAsync (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Nil)); + Assert.That (token.ToString (), Is.EqualTo ("NIL")); + + token = stream.ReadToken (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Nil)); + Assert.That (token.ToString (), Is.EqualTo ("Nil")); + + token = stream.ReadToken (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Nil)); + Assert.That (token.ToString (), Is.EqualTo ("nil")); + + token = await stream.ReadTokenAsync (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.CloseParen)); + Assert.That (token.ToString (), Is.EqualTo ("')'")); + + token = await stream.ReadTokenAsync (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.OpenBracket)); + Assert.That (token.ToString (), Is.EqualTo ("'['")); + + token = await stream.ReadTokenAsync (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.CloseBracket)); + Assert.That (token.ToString (), Is.EqualTo ("']'")); + + token = await stream.ReadTokenAsync (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln)); + Assert.That (token.ToString (), Is.EqualTo ("'\\n'")); + + stream.UngetToken (token); + token = await stream.ReadTokenAsync (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln)); + Assert.That (token.ToString (), Is.EqualTo ("'\\n'")); + } + } + + [Test] + public void TestReadContinuationToken () + { + using (var stream = new ImapStream (new DummyNetworkStream (), new NullProtocolLogger ())) { + var data = Encoding.ASCII.GetBytes ("+ Please continue...\r\n"); + + stream.Stream.Write (data, 0, data.Length); + stream.Stream.Position = 0; + + var token = stream.ReadToken (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Atom)); + Assert.That (token.ToString (), Is.EqualTo ("+")); + } + } + + [Test] + public async Task TestReadContinuationTokenAsync () + { + using (var stream = new ImapStream (new DummyNetworkStream (), new NullProtocolLogger ())) { + var data = Encoding.ASCII.GetBytes ("+ Please continue...\r\n"); + + stream.Stream.Write (data, 0, data.Length); + stream.Stream.Position = 0; + + var token = await stream.ReadTokenAsync (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Atom)); + Assert.That (token.ToString (), Is.EqualTo ("+")); + } + } + + [Test] + public void TestReadBrokenLiteralToken () + { + using (var stream = new ImapStream (new DummyNetworkStream (), new NullProtocolLogger ())) { + var data = Encoding.ASCII.GetBytes ("{4096+" + new string (' ', 4096) + "}" + new string (' ', 4096) + "\r\n"); + + stream.Stream.Write (data, 0, data.Length); + stream.Stream.Position = 0; + + var token = stream.ReadToken (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Literal)); + Assert.That (token.ToString (), Is.EqualTo ("{4096}")); + } + } + + [Test] + public async Task TestReadBrokenLiteralTokenAsync () + { + using (var stream = new ImapStream (new DummyNetworkStream (), new NullProtocolLogger ())) { + var data = Encoding.ASCII.GetBytes ("{4096+" + new string (' ', 4096) + "}" + new string (' ', 4096) + "\r\n"); + + stream.Stream.Write (data, 0, data.Length); + stream.Stream.Position = 0; + + var token = await stream.ReadTokenAsync (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Literal)); + Assert.That (token.ToString (), Is.EqualTo ("{4096}")); + } + } + + [Test] + public void TestSeek () + { + using (var stream = new ImapStream (new DummyNetworkStream (), new NullProtocolLogger ())) { + Assert.Throws (() => stream.Seek (0, SeekOrigin.Begin)); + Assert.Throws (() => stream.Position = 500); + Assert.That (stream.Position, Is.EqualTo (0)); + Assert.That (stream.Length, Is.EqualTo (0)); + } + } + + [Test] + public void TestSetLength () + { + using (var stream = new ImapStream (new DummyNetworkStream (), new NullProtocolLogger ())) { + Assert.Throws (() => stream.SetLength (500)); + } + } + + [Test] + public void TestWrite () + { + using (var stream = new ImapStream (new DummyNetworkStream (), new NullProtocolLogger ())) { + var buf1k = RandomNumberGenerator.GetBytes (1024); + var buf4k = RandomNumberGenerator.GetBytes (4096); + var buf9k = RandomNumberGenerator.GetBytes (9216); + var memory = (MemoryStream) stream.Stream; + var buffer = new byte[8192]; + byte[] mem; + + Assert.Throws (() => stream.Write (null, 0, buffer.Length)); + Assert.Throws (() => stream.Write (buffer, -1, buffer.Length)); + Assert.Throws (() => stream.Write (buffer, 0, -1)); + + // Test #1: write less than 4K to make sure that ImapStream buffers it + stream.Write (buf1k, 0, buf1k.Length); + Assert.That (memory.Length, Is.EqualTo (0), "#1"); + + // Test #2: make sure that flushing the ImapStream flushes the entire buffer out to the network + stream.Flush (); + Assert.That (memory.Length, Is.EqualTo (buf1k.Length), "#2"); + mem = memory.GetBuffer (); + for (int i = 0; i < buf1k.Length; i++) + Assert.That (mem[i], Is.EqualTo (buf1k[i]), $"#2 byte[{i}]"); + memory.SetLength (0); + + // Test #3: write exactly 4K to make sure it passes through w/o the need to flush + stream.Write (buf4k, 0, buf4k.Length); + Assert.That (memory.Length, Is.EqualTo (buf4k.Length), "#3"); + mem = memory.GetBuffer (); + for (int i = 0; i < buf4k.Length; i++) + Assert.That (mem[i], Is.EqualTo (buf4k[i]), $"#3 byte[{i}]"); + memory.SetLength (0); + + // Test #4: write 1k and then write 4k, make sure that only 4k passes thru (last 1k gets buffered) + stream.Write (buf1k, 0, buf1k.Length); + stream.Write (buf4k, 0, buf4k.Length); + Assert.That (memory.Length, Is.EqualTo (4096), "#4"); + stream.Flush (); + Assert.That (memory.Length, Is.EqualTo (buf1k.Length + buf4k.Length), "#4"); + Array.Copy (buf1k, 0, buffer, 0, buf1k.Length); + Array.Copy (buf4k, 0, buffer, buf1k.Length, buf4k.Length); + mem = memory.GetBuffer (); + for (int i = 0; i < buf1k.Length + buf4k.Length; i++) + Assert.That (mem[i], Is.EqualTo (buffer[i]), $"#4 byte[{i}]"); + memory.SetLength (0); + + // Test #5: write 9k and make sure only the first 8k goes thru (last 1k gets buffered) + stream.Write (buf9k, 0, buf9k.Length); + Assert.That (memory.Length, Is.EqualTo (8192), "#5"); + stream.Flush (); + Assert.That (memory.Length, Is.EqualTo (buf9k.Length), "#5"); + mem = memory.GetBuffer (); + for (int i = 0; i < buf9k.Length; i++) + Assert.That (mem[i], Is.EqualTo (buf9k[i]), $"#5 byte[{i}]"); + memory.SetLength (0); + } + } + + [Test] + public async Task TestWriteAsync () + { + using (var stream = new ImapStream (new DummyNetworkStream (), new NullProtocolLogger ())) { + var buf1k = RandomNumberGenerator.GetBytes (1024); + var buf4k = RandomNumberGenerator.GetBytes (4096); + var buf9k = RandomNumberGenerator.GetBytes (9216); + var memory = (MemoryStream) stream.Stream; + var buffer = new byte[8192]; + byte[] mem; + + // Test #1: write less than 4K to make sure that ImapStream buffers it + await stream.WriteAsync (buf1k, 0, buf1k.Length); + Assert.That (memory.Length, Is.EqualTo (0), "#1"); + + // Test #2: make sure that flushing the ImapStream flushes the entire buffer out to the network + await stream.FlushAsync (); + Assert.That (memory.Length, Is.EqualTo (buf1k.Length), "#2"); + mem = memory.GetBuffer (); + for (int i = 0; i < buf1k.Length; i++) + Assert.That (mem[i], Is.EqualTo (buf1k[i]), $"#2 byte[{i}]"); + memory.SetLength (0); + + // Test #3: write exactly 4K to make sure it passes through w/o the need to flush + await stream.WriteAsync (buf4k, 0, buf4k.Length); + Assert.That (memory.Length, Is.EqualTo (buf4k.Length), "#3"); + mem = memory.GetBuffer (); + for (int i = 0; i < buf4k.Length; i++) + Assert.That (mem[i], Is.EqualTo (buf4k[i]), $"#3 byte[{i}]"); + memory.SetLength (0); + + // Test #4: write 1k and then write 4k, make sure that only 4k passes thru (last 1k gets buffered) + await stream.WriteAsync (buf1k, 0, buf1k.Length); + await stream.WriteAsync (buf4k, 0, buf4k.Length); + Assert.That (memory.Length, Is.EqualTo (4096), "#4"); + await stream.FlushAsync (); + Assert.That (memory.Length, Is.EqualTo (buf1k.Length + buf4k.Length), "#4"); + Array.Copy (buf1k, 0, buffer, 0, buf1k.Length); + Array.Copy (buf4k, 0, buffer, buf1k.Length, buf4k.Length); + mem = memory.GetBuffer (); + for (int i = 0; i < buf1k.Length + buf4k.Length; i++) + Assert.That (mem[i], Is.EqualTo (buffer[i]), $"#4 byte[{i}]"); + memory.SetLength (0); + + // Test #5: write 9k and make sure only the first 8k goes thru (last 1k gets buffered) + await stream.WriteAsync (buf9k, 0, buf9k.Length); + Assert.That (memory.Length, Is.EqualTo (8192), "#5"); + await stream.FlushAsync (); + Assert.That (memory.Length, Is.EqualTo (buf9k.Length), "#5"); + mem = memory.GetBuffer (); + for (int i = 0; i < buf9k.Length; i++) + Assert.That (mem[i], Is.EqualTo (buf9k[i]), $"#5 byte[{i}]"); + memory.SetLength (0); + } + } + } +} diff --git a/UnitTests/Net/Imap/ImapUtilsTests.cs b/UnitTests/Net/Imap/ImapUtilsTests.cs index a2c32697ef..0fbac12b60 100644 --- a/UnitTests/Net/Imap/ImapUtilsTests.cs +++ b/UnitTests/Net/Imap/ImapUtilsTests.cs @@ -1,9 +1,9 @@ -// +// // ImapBodyParsingTests.cs // // Author: Jeffrey Stedfast // -// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// Copyright (c) 2013-2026 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -24,25 +24,24 @@ // THE SOFTWARE. // -using System; -using System.IO; -using System.Linq; using System.Text; -using System.Threading; -using System.Collections.Generic; - -using NUnit.Framework; - -using MimeKit; using MimeKit.Utils; -using MailKit.Net.Imap; using MailKit; +using MailKit.Net.Imap; namespace UnitTests.Net.Imap { [TestFixture] - public class ImapUtilsTests + public class ImapUtilsTests : IDisposable { + readonly ImapEngine engine = new ImapEngine (null); + + public void Dispose () + { + engine.Dispose (); + GC.SuppressFinalize (this); + } + [Test] public void TestResponseCodeCreation () { @@ -50,6 +49,56 @@ public void TestResponseCodeCreation () Assert.DoesNotThrow (() => ImapResponseCode.Create (type)); } + [Test] + public void TestFormattingSimpleIndexRange () + { + int[] indexes = { 0, 1, 2, 3, 4, 5, 6, 7, 8 }; + const string expect = "1:9"; + string actual; + + Assert.Throws (() => ImapUtils.FormatIndexSet (null, new int[1])); + Assert.Throws (() => ImapUtils.FormatIndexSet (engine, null)); + Assert.Throws (() => ImapUtils.FormatIndexSet (engine, Array.Empty ())); + + Assert.Throws (() => ImapUtils.FormatIndexSet (engine, null, new int[1])); + + actual = ImapUtils.FormatIndexSet (engine, indexes); + Assert.That (actual, Is.EqualTo (expect), "Formatting a simple range of indexes failed."); + } + + [Test] + public void TestFormattingNonSequentialIndexes () + { + int[] indexes = { 0, 2, 4, 6, 8 }; + const string expect = "1,3,5,7,9"; + string actual; + + actual = ImapUtils.FormatIndexSet (engine, indexes); + Assert.That (actual, Is.EqualTo (expect), "Formatting a non-sequential list of indexes."); + } + + [Test] + public void TestFormattingComplexSetOfIndexes () + { + int[] indexes = { 0, 1, 2, 4, 5, 8, 9, 10, 11, 14, 18, 19 }; + const string expect = "1:3,5:6,9:12,15,19:20"; + string actual; + + actual = ImapUtils.FormatIndexSet (engine, indexes); + Assert.That (actual, Is.EqualTo (expect), "Formatting a complex list of indexes."); + } + + [Test] + public void TestFormattingReversedIndexes () + { + int[] indexes = { 19, 18, 14, 11, 10, 9, 8, 5, 4, 2, 1, 0 }; + const string expect = "20:19,15,12:9,6:5,3:1"; + string actual; + + actual = ImapUtils.FormatIndexSet (engine, indexes); + Assert.That (actual, Is.EqualTo (expect), "Formatting a complex list of indexes."); + } + [Test] public void TestFormattingSimpleUidRange () { @@ -61,8 +110,8 @@ public void TestFormattingSimpleUidRange () const string expect = "1:9"; string actual; - actual = ImapUtils.FormatUidSet (uids); - Assert.AreEqual (expect, actual, "Formatting a simple range of uids failed."); + actual = UniqueIdSet.ToString (uids); + Assert.That (actual, Is.EqualTo (expect), "Formatting a simple range of uids failed."); } [Test] @@ -75,8 +124,8 @@ public void TestFormattingNonSequentialUids () const string expect = "1,3,5,7,9"; string actual; - actual = ImapUtils.FormatUidSet (uids); - Assert.AreEqual (expect, actual, "Formatting a non-sequential list of uids."); + actual = UniqueIdSet.ToString (uids); + Assert.That (actual, Is.EqualTo (expect), "Formatting a non-sequential list of uids."); } [Test] @@ -91,8 +140,8 @@ public void TestFormattingComplexSetOfUids () const string expect = "1:3,5:6,9:12,15,19:20"; string actual; - actual = ImapUtils.FormatUidSet (uids); - Assert.AreEqual (expect, actual, "Formatting a complex list of uids."); + actual = UniqueIdSet.ToString (uids); + Assert.That (actual, Is.EqualTo (expect), "Formatting a complex list of uids."); } [Test] @@ -107,8 +156,36 @@ public void TestFormattingReversedUids () const string expect = "20:19,15,12:9,6:5,3:1"; string actual; - actual = ImapUtils.FormatUidSet (uids); - Assert.AreEqual (expect, actual, "Formatting a complex list of uids."); + actual = UniqueIdSet.ToString (uids); + Assert.That (actual, Is.EqualTo (expect), "Formatting a complex list of uids."); + } + + [Test] + public void TestParseInvalidInternalDates () + { + var internalDates = new string [] { + "00-Jan-0000 00:00:00 +0000", // Note: This example is taken from an actual response from a Domino IMAP server. Likely represents an uninitialized value. + "98765432100-OCT-2018 13:41:57 -0400", + "27-JAG-2018 13:41:57 -0400", + "27-OCT-1909 13:41:57 -0400", + "27-OCT-2018 33:41:57 -0400", + "27-OCT-2018 13:411:57 -0400", + "27-OCT-2018 13:41:577 -0400", + "27-OCT-2018 13:41:577 -98765432100", + "27-OCT-2018 13:41:57 -0400 XYZ", + }; + + foreach (var internalDate in internalDates) + Assert.That (ImapUtils.ParseInternalDate (internalDate), Is.EqualTo (DateTimeOffset.MinValue), internalDate); + } + + [Test] + public void TestCanonicalizeMailboxName () + { + Assert.That (ImapUtils.CanonicalizeMailboxName ("Name", '.'), Is.EqualTo ("Name"), "Name"); + Assert.That (ImapUtils.CanonicalizeMailboxName ("InbOx", '.'), Is.EqualTo ("INBOX"), "InbOx"); + Assert.That (ImapUtils.CanonicalizeMailboxName ("InboxSubfolder", '.'), Is.EqualTo ("InboxSubfolder"), "InboxSubfolder"); + Assert.That (ImapUtils.CanonicalizeMailboxName ("Inbox.Subfolder", '.'), Is.EqualTo ("INBOX.Subfolder"), "Inbox.Subfolder"); } [Test] @@ -117,7 +194,7 @@ public void TestParseLabelsListWithNIL () const string text = "(atom-label \\flag-label \"quoted-label\" NIL)\r\n"; using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { - using (var tokenizer = new ImapStream (memory, null, new NullProtocolLogger ())) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { using (var engine = new ImapEngine (null)) { IList labels; @@ -126,12 +203,38 @@ public void TestParseLabelsListWithNIL () try { labels = ImapUtils.ParseLabelsList (engine, CancellationToken.None); } catch (Exception ex) { - Assert.Fail ("Parsing X-GM-LABELS failed: {0}", ex); + Assert.Fail ($"Parsing X-GM-LABELS failed: {ex}"); return; } var token = engine.ReadToken (CancellationToken.None); - Assert.AreEqual (ImapTokenType.Eoln, token.Type, "Expected new-line, but got: {0}", token); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + } + } + } + } + + [Test] + public async Task TestParseLabelsListWithNILAsync () + { + const string text = "(atom-label \\flag-label \"quoted-label\" NIL)\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + IList labels; + + engine.SetStream (tokenizer); + + try { + labels = await ImapUtils.ParseLabelsListAsync (engine, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing X-GM-LABELS failed: {ex}"); + return; + } + + var token = await engine.ReadTokenAsync (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); } } } @@ -143,7 +246,7 @@ public void TestParseExampleBodyRfc3501 () const string text = "(\"TEXT\" \"PLAIN\" (\"CHARSET\" \"US-ASCII\") NIL NIL \"7BIT\" 3028 92)\r\n"; using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { - using (var tokenizer = new ImapStream (memory, null, new NullProtocolLogger ())) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { using (var engine = new ImapEngine (null)) { BodyPartText basic; BodyPart body; @@ -153,23 +256,61 @@ public void TestParseExampleBodyRfc3501 () try { body = ImapUtils.ParseBody (engine, "Unexpected token: {0}", string.Empty, CancellationToken.None); } catch (Exception ex) { - Assert.Fail ("Parsing BODY failed: {0}", ex); + Assert.Fail ($"Parsing BODY failed: {ex}"); return; } var token = engine.ReadToken (CancellationToken.None); - Assert.AreEqual (ImapTokenType.Eoln, token.Type, "Expected new-line, but got: {0}", token); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (body, Is.InstanceOf (), "Body types did not match."); + basic = (BodyPartText) body; + + Assert.That (body.ContentType.IsMimeType ("text", "plain"), Is.True, "Content-Type did not match."); + Assert.That (body.ContentType.Parameters["charset"], Is.EqualTo ("US-ASCII"), "charset param did not match"); + + Assert.That (basic, Is.Not.Null, "The parsed body is not BodyPartText."); + Assert.That (basic.ContentTransferEncoding, Is.EqualTo ("7BIT"), "Content-Transfer-Encoding did not match."); + Assert.That (basic.Octets, Is.EqualTo (3028), "Octet count did not match."); + Assert.That (basic.Lines, Is.EqualTo (92), "Line count did not match."); + } + } + } + } + + [Test] + public async Task TestParseExampleBodyRfc3501Async () + { + const string text = "(\"TEXT\" \"PLAIN\" (\"CHARSET\" \"US-ASCII\") NIL NIL \"7BIT\" 3028 92)\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + BodyPartText basic; + BodyPart body; + + engine.SetStream (tokenizer); - Assert.IsInstanceOf (body, "Body types did not match."); + try { + body = await ImapUtils.ParseBodyAsync (engine, "Unexpected token: {0}", string.Empty, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing BODY failed: {ex}"); + return; + } + + var token = await engine.ReadTokenAsync (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (body, Is.InstanceOf (), "Body types did not match."); basic = (BodyPartText) body; - Assert.IsTrue (body.ContentType.IsMimeType ("text", "plain"), "Content-Type did not match."); - Assert.AreEqual ("US-ASCII", body.ContentType.Parameters["charset"], "charset param did not match"); + Assert.That (body.ContentType.IsMimeType ("text", "plain"), Is.True, "Content-Type did not match."); + Assert.That (body.ContentType.Parameters["charset"], Is.EqualTo ("US-ASCII"), "charset param did not match"); - Assert.IsNotNull (basic, "The parsed body is not BodyPartText."); - Assert.AreEqual ("7BIT", basic.ContentTransferEncoding, "Content-Transfer-Encoding did not match."); - Assert.AreEqual (3028, basic.Octets, "Octet count did not match."); - Assert.AreEqual (92, basic.Lines, "Line count did not match."); + Assert.That (basic, Is.Not.Null, "The parsed body is not BodyPartText."); + Assert.That (basic.ContentTransferEncoding, Is.EqualTo ("7BIT"), "Content-Transfer-Encoding did not match."); + Assert.That (basic.Octets, Is.EqualTo (3028), "Octet count did not match."); + Assert.That (basic.Lines, Is.EqualTo (92), "Line count did not match."); } } } @@ -181,7 +322,7 @@ public void TestParseExampleEnvelopeRfc3501 () const string text = "(\"Wed, 17 Jul 1996 02:23:25 -0700 (PDT)\" \"IMAP4rev1 WG mtg summary and minutes\" ((\"Terry Gray\" NIL \"gray\" \"cac.washington.edu\")) ((\"Terry Gray\" NIL \"gray\" \"cac.washington.edu\")) ((\"Terry Gray\" NIL \"gray\" \"cac.washington.edu\")) ((NIL NIL \"imap\" \"cac.washington.edu\")) ((NIL NIL \"minutes\" \"CNRI.Reston.VA.US\") (\"John Klensin\" NIL \"KLENSIN\" \"MIT.EDU\")) NIL NIL \"\")\r\n"; using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { - using (var tokenizer = new ImapStream (memory, null, new NullProtocolLogger ())) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { using (var engine = new ImapEngine (null)) { Envelope envelope; @@ -190,95 +331,100 @@ public void TestParseExampleEnvelopeRfc3501 () try { envelope = ImapUtils.ParseEnvelope (engine, CancellationToken.None); } catch (Exception ex) { - Assert.Fail ("Parsing ENVELOPE failed: {0}", ex); + Assert.Fail ($"Parsing ENVELOPE failed: {ex}"); return; } var token = engine.ReadToken (CancellationToken.None); - Assert.AreEqual (ImapTokenType.Eoln, token.Type, "Expected new-line, but got: {0}", token); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); - Assert.IsTrue (envelope.Date.HasValue, "Parsed ENVELOPE date is null."); - Assert.AreEqual ("Wed, 17 Jul 1996 02:23:25 -0700", DateUtils.FormatDate (envelope.Date.Value), "Date does not match."); - Assert.AreEqual ("IMAP4rev1 WG mtg summary and minutes", envelope.Subject, "Subject does not match."); + Assert.That (envelope.Date.HasValue, Is.True, "Parsed ENVELOPE date is null."); + Assert.That (DateUtils.FormatDate (envelope.Date.Value), Is.EqualTo ("Wed, 17 Jul 1996 02:23:25 -0700"), "Date does not match."); + Assert.That (envelope.Subject, Is.EqualTo ("IMAP4rev1 WG mtg summary and minutes"), "Subject does not match."); - Assert.AreEqual (1, envelope.From.Count, "From counts do not match."); - Assert.AreEqual ("\"Terry Gray\" ", envelope.From.ToString (), "From does not match."); + Assert.That (envelope.From, Has.Count.EqualTo (1), "From counts do not match."); + Assert.That (envelope.From.ToString (), Is.EqualTo ("\"Terry Gray\" "), "From does not match."); - Assert.AreEqual (1, envelope.Sender.Count, "Sender counts do not match."); - Assert.AreEqual ("\"Terry Gray\" ", envelope.Sender.ToString (), "Sender does not match."); + Assert.That (envelope.Sender, Has.Count.EqualTo (1), "Sender counts do not match."); + Assert.That (envelope.Sender.ToString (), Is.EqualTo ("\"Terry Gray\" "), "Sender does not match."); - Assert.AreEqual (1, envelope.ReplyTo.Count, "Reply-To counts do not match."); - Assert.AreEqual ("\"Terry Gray\" ", envelope.ReplyTo.ToString (), "Reply-To does not match."); + Assert.That (envelope.ReplyTo, Has.Count.EqualTo (1), "Reply-To counts do not match."); + Assert.That (envelope.ReplyTo.ToString (), Is.EqualTo ("\"Terry Gray\" "), "Reply-To does not match."); - Assert.AreEqual (1, envelope.To.Count, "To counts do not match."); - Assert.AreEqual ("imap@cac.washington.edu", envelope.To.ToString (), "To does not match."); + Assert.That (envelope.To, Has.Count.EqualTo (1), "To counts do not match."); + Assert.That (envelope.To.ToString (), Is.EqualTo ("imap@cac.washington.edu"), "To does not match."); - Assert.AreEqual (2, envelope.Cc.Count, "Cc counts do not match."); - Assert.AreEqual ("minutes@CNRI.Reston.VA.US, \"John Klensin\" ", envelope.Cc.ToString (), "Cc does not match."); + Assert.That (envelope.Cc, Has.Count.EqualTo (2), "Cc counts do not match."); + Assert.That (envelope.Cc.ToString (), Is.EqualTo ("minutes@CNRI.Reston.VA.US, \"John Klensin\" "), "Cc does not match."); - Assert.AreEqual (0, envelope.Bcc.Count, "Bcc counts do not match."); + Assert.That (envelope.Bcc, Is.Empty, "Bcc counts do not match."); - Assert.IsNull (envelope.InReplyTo, "In-Reply-To is not null."); + Assert.That (envelope.InReplyTo, Is.Null, "In-Reply-To is not null."); - Assert.AreEqual ("B27397-0100000@cac.washington.edu", envelope.MessageId, "Message-Id does not match."); + Assert.That (envelope.MessageId, Is.EqualTo ("B27397-0100000@cac.washington.edu"), "Message-Id does not match."); } } } } [Test] - public void TestParseMalformedMailboxAddressInEnvelope () + public async Task TestParseExampleEnvelopeRfc3501Async () { - const string text = "(\"Mon, 10 Apr 2017 06:04:00 -0700\" \"Session 2: Building the meditation habit\" ((\"Headspace\" NIL \"members\" \"headspace.com\")) ((NIL NIL \"\")) ((\"Headspace\" NIL \"members\" \"headspace.com\")) ((NIL NIL \"user\" \"gmail.com\")) NIL NIL NIL \"\")"; + const string text = "(\"Wed, 17 Jul 1996 02:23:25 -0700 (PDT)\" \"IMAP4rev1 WG mtg summary and minutes\" ((\"Terry Gray\" NIL \"gray\" \"cac.washington.edu\")) ((\"Terry Gray\" NIL \"gray\" \"cac.washington.edu\")) ((\"Terry Gray\" NIL \"gray\" \"cac.washington.edu\")) ((NIL NIL \"imap\" \"cac.washington.edu\")) ((NIL NIL \"minutes\" \"CNRI.Reston.VA.US\") (\"John Klensin\" NIL \"KLENSIN\" \"MIT.EDU\")) NIL NIL \"\")\r\n"; using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { - using (var tokenizer = new ImapStream (memory, null, new NullProtocolLogger ())) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { using (var engine = new ImapEngine (null)) { Envelope envelope; engine.SetStream (tokenizer); try { - envelope = ImapUtils.ParseEnvelope (engine, CancellationToken.None); + envelope = await ImapUtils.ParseEnvelopeAsync (engine, CancellationToken.None); } catch (Exception ex) { - Assert.Fail ("Parsing ENVELOPE failed: {0}", ex); + Assert.Fail ($"Parsing ENVELOPE failed: {ex}"); return; } - Assert.IsTrue (envelope.Date.HasValue, "Parsed ENVELOPE date is null."); - Assert.AreEqual ("Mon, 10 Apr 2017 06:04:00 -0700", DateUtils.FormatDate (envelope.Date.Value), "Date does not match."); - Assert.AreEqual ("Session 2: Building the meditation habit", envelope.Subject, "Subject does not match."); + var token = await engine.ReadTokenAsync (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (envelope.Date.HasValue, Is.True, "Parsed ENVELOPE date is null."); + Assert.That (DateUtils.FormatDate (envelope.Date.Value), Is.EqualTo ("Wed, 17 Jul 1996 02:23:25 -0700"), "Date does not match."); + Assert.That (envelope.Subject, Is.EqualTo ("IMAP4rev1 WG mtg summary and minutes"), "Subject does not match."); + + Assert.That (envelope.From, Has.Count.EqualTo (1), "From counts do not match."); + Assert.That (envelope.From.ToString (), Is.EqualTo ("\"Terry Gray\" "), "From does not match."); - Assert.AreEqual (1, envelope.From.Count, "From counts do not match."); - Assert.AreEqual ("\"Headspace\" ", envelope.From.ToString (), "From does not match."); + Assert.That (envelope.Sender, Has.Count.EqualTo (1), "Sender counts do not match."); + Assert.That (envelope.Sender.ToString (), Is.EqualTo ("\"Terry Gray\" "), "Sender does not match."); - Assert.AreEqual (1, envelope.Sender.Count, "Sender counts do not match."); - Assert.AreEqual ("members=headspace.com@members.headspace.com", envelope.Sender.ToString (), "Sender does not match."); + Assert.That (envelope.ReplyTo, Has.Count.EqualTo (1), "Reply-To counts do not match."); + Assert.That (envelope.ReplyTo.ToString (), Is.EqualTo ("\"Terry Gray\" "), "Reply-To does not match."); - Assert.AreEqual (1, envelope.ReplyTo.Count, "Reply-To counts do not match."); - Assert.AreEqual ("\"Headspace\" ", envelope.ReplyTo.ToString (), "Reply-To does not match."); + Assert.That (envelope.To, Has.Count.EqualTo (1), "To counts do not match."); + Assert.That (envelope.To.ToString (), Is.EqualTo ("imap@cac.washington.edu"), "To does not match."); - Assert.AreEqual (1, envelope.To.Count, "To counts do not match."); - Assert.AreEqual ("user@gmail.com", envelope.To.ToString (), "To does not match."); + Assert.That (envelope.Cc, Has.Count.EqualTo (2), "Cc counts do not match."); + Assert.That (envelope.Cc.ToString (), Is.EqualTo ("minutes@CNRI.Reston.VA.US, \"John Klensin\" "), "Cc does not match."); - Assert.AreEqual (0, envelope.Cc.Count, "Cc counts do not match."); - Assert.AreEqual (0, envelope.Bcc.Count, "Bcc counts do not match."); + Assert.That (envelope.Bcc, Is.Empty, "Bcc counts do not match."); - Assert.IsNull (envelope.InReplyTo, "In-Reply-To is not null."); + Assert.That (envelope.InReplyTo, Is.Null, "In-Reply-To is not null."); - Assert.AreEqual ("bvqyalstpemxt9y3afoqh4an62b2arcd.rcd.1491829440@members.headspace.com", envelope.MessageId, "Message-Id does not match."); + Assert.That (envelope.MessageId, Is.EqualTo ("B27397-0100000@cac.washington.edu"), "Message-Id does not match."); } } } } [Test] - public void TestParseDovcotEnvelopeWithGroupAddresses () + public void TestParseExampleEnvelopeRfc3501WithLiterals () { - const string text = "(\"Mon, 13 Jul 2015 21:15:32 -0400\" \"Test message\" ((\"Example From\" NIL \"from\" \"example.com\")) ((\"Example Sender\" NIL \"sender\" \"example.com\")) ((\"Example Reply-To\" NIL \"reply-to\" \"example.com\")) ((NIL NIL \"boys\" NIL)(NIL NIL \"aaron\" \"MISSING_DOMAIN\")(NIL NIL \"jeff\" \"MISSING_DOMAIN\")(NIL NIL \"zach\" \"MISSING_DOMAIN\")(NIL NIL NIL NIL)(NIL NIL \"girls\" NIL)(NIL NIL \"alice\" \"MISSING_DOMAIN\")(NIL NIL \"hailey\" \"MISSING_DOMAIN\")(NIL NIL \"jenny\" \"MISSING_DOMAIN\")(NIL NIL NIL NIL)) NIL NIL NIL \"\")"; + const string text = "({37}\r\nWed, 17 Jul 1996 02:23:25 -0700 (PDT) {36}\r\nIMAP4rev1 WG mtg summary and minutes (({10}\r\nTerry Gray NIL {4}\r\ngray \"cac.washington.edu\")) ((\"Terry Gray\" NIL \"gray\" \"cac.washington.edu\")) ((\"Terry Gray\" NIL \"gray\" \"cac.washington.edu\")) ((NIL NIL \"imap\" \"cac.washington.edu\")) ((NIL NIL \"minutes\" \"CNRI.Reston.VA.US\") (\"John Klensin\" NIL \"KLENSIN\" \"MIT.EDU\")) NIL NIL {35}\r\n)\r\n"; using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { - using (var tokenizer = new ImapStream (memory, null, new NullProtocolLogger ())) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { using (var engine = new ImapEngine (null)) { Envelope envelope; @@ -287,198 +433,4099 @@ public void TestParseDovcotEnvelopeWithGroupAddresses () try { envelope = ImapUtils.ParseEnvelope (engine, CancellationToken.None); } catch (Exception ex) { - Assert.Fail ("Parsing ENVELOPE failed: {0}", ex); + Assert.Fail ($"Parsing ENVELOPE failed: {ex}"); return; } - Assert.AreEqual ("\"Example Sender\" ", envelope.Sender.ToString ()); - Assert.AreEqual ("\"Example From\" ", envelope.From.ToString ()); - Assert.AreEqual ("\"Example Reply-To\" ", envelope.ReplyTo.ToString ()); - Assert.AreEqual ("boys: aaron, jeff, zach;, girls: alice, hailey, jenny;", envelope.To.ToString ()); + var token = engine.ReadToken (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (envelope.Date.HasValue, Is.True, "Parsed ENVELOPE date is null."); + Assert.That (DateUtils.FormatDate (envelope.Date.Value), Is.EqualTo ("Wed, 17 Jul 1996 02:23:25 -0700"), "Date does not match."); + Assert.That (envelope.Subject, Is.EqualTo ("IMAP4rev1 WG mtg summary and minutes"), "Subject does not match."); + + Assert.That (envelope.From, Has.Count.EqualTo (1), "From counts do not match."); + Assert.That (envelope.From.ToString (), Is.EqualTo ("\"Terry Gray\" "), "From does not match."); + + Assert.That (envelope.Sender, Has.Count.EqualTo (1), "Sender counts do not match."); + Assert.That (envelope.Sender.ToString (), Is.EqualTo ("\"Terry Gray\" "), "Sender does not match."); + + Assert.That (envelope.ReplyTo, Has.Count.EqualTo (1), "Reply-To counts do not match."); + Assert.That (envelope.ReplyTo.ToString (), Is.EqualTo ("\"Terry Gray\" "), "Reply-To does not match."); + + Assert.That (envelope.To, Has.Count.EqualTo (1), "To counts do not match."); + Assert.That (envelope.To.ToString (), Is.EqualTo ("imap@cac.washington.edu"), "To does not match."); + + Assert.That (envelope.Cc, Has.Count.EqualTo (2), "Cc counts do not match."); + Assert.That (envelope.Cc.ToString (), Is.EqualTo ("minutes@CNRI.Reston.VA.US, \"John Klensin\" "), "Cc does not match."); + + Assert.That (envelope.Bcc, Is.Empty, "Bcc counts do not match."); + + Assert.That (envelope.InReplyTo, Is.Null, "In-Reply-To is not null."); + + Assert.That (envelope.MessageId, Is.EqualTo ("B27397-0100000@cac.washington.edu"), "Message-Id does not match."); } } } } [Test] - public void TestParseExampleMultiLevelDovecotBodyStructure () + public async Task TestParseExampleEnvelopeRfc3501WithLiteralsAsync () { - const string text = "(((\"text\" \"plain\" (\"charset\" \"iso-8859-2\") NIL NIL \"quoted-printable\" 28 2 NIL NIL NIL NIL) (\"text\" \"html\" (\"charset\" \"iso-8859-2\") NIL NIL \"quoted-printable\" 1707 65 NIL NIL NIL NIL) \"alternative\" (\"boundary\" \"----=_NextPart_001_0078_01CBB179.57530990\") NIL NIL NIL) (\"message\" \"rfc822\" NIL NIL NIL \"7bit\" 641 (\"Sat, 8 Jan 2011 14:16:36 +0100\" \"Subj 2\" ((\"Some Name, SOMECOMPANY\" NIL \"recipient\" \"example.com\")) ((\"Some Name, SOMECOMPANY\" NIL \"recipient\" \"example.com\")) ((\"Some Name, SOMECOMPANY\" NIL \"recipient\" \"example.com\")) ((\"Recipient\" NIL \"example\" \"gmail.com\")) NIL NIL NIL NIL) (\"text\" \"plain\" (\"charset\" \"iso-8859-2\") NIL NIL \"quoted-printable\" 185 18 NIL NIL (\"cs\") NIL) 31 NIL (\"attachment\" NIL) NIL NIL) (\"message\" \"rfc822\" NIL NIL NIL \"7bit\" 50592 (\"Sat, 8 Jan 2011 13:58:39 +0100\" \"Subj 1\" ((\"Some Name, SOMECOMPANY\" NIL \"recipient\" \"example.com\")) ((\"Some Name, SOMECOMPANY\" NIL \"recipient\" \"example.com\")) ((\"Some Name, SOMECOMPANY\" NIL \"recipient\" \"example.com\")) ((\"Recipient\" NIL \"example\" \"gmail.com\")) NIL NIL NIL NIL) ( (\"text\" \"plain\" (\"charset\" \"iso-8859-2\") NIL NIL \"quoted-printable\" 4296 345 NIL NIL NIL NIL) (\"text\" \"html\" (\"charset\" \"iso-8859-2\") NIL NIL \"quoted-printable\" 45069 1295 NIL NIL NIL NIL) \"alternative\" (\"boundary\" \"----=_NextPart_000_0073_01CBB179.57530990\") NIL (\"cs\") NIL) 1669 NIL (\"attachment\" NIL) NIL NIL) \"mixed\" (\"boundary\" \"----=_NextPart_000_0077_01CBB179.57530990\") NIL (\"cs\") NIL)\r\n"; + const string text = "({37}\r\nWed, 17 Jul 1996 02:23:25 -0700 (PDT) {36}\r\nIMAP4rev1 WG mtg summary and minutes (({10}\r\nTerry Gray NIL {4}\r\ngray \"cac.washington.edu\")) ((\"Terry Gray\" NIL \"gray\" \"cac.washington.edu\")) ((\"Terry Gray\" NIL \"gray\" \"cac.washington.edu\")) ((NIL NIL \"imap\" \"cac.washington.edu\")) ((NIL NIL \"minutes\" \"CNRI.Reston.VA.US\") (\"John Klensin\" NIL \"KLENSIN\" \"MIT.EDU\")) NIL NIL {35}\r\n)\r\n"; using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { - using (var tokenizer = new ImapStream (memory, null, new NullProtocolLogger ())) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { using (var engine = new ImapEngine (null)) { - BodyPartMultipart multipart; - BodyPart body; + Envelope envelope; engine.SetStream (tokenizer); try { - body = ImapUtils.ParseBody (engine, "Unexpected token: {0}", string.Empty, CancellationToken.None); + envelope = await ImapUtils.ParseEnvelopeAsync (engine, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing ENVELOPE failed: {ex}"); + return; + } + + var token = await engine.ReadTokenAsync (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (envelope.Date.HasValue, Is.True, "Parsed ENVELOPE date is null."); + Assert.That (DateUtils.FormatDate (envelope.Date.Value), Is.EqualTo ("Wed, 17 Jul 1996 02:23:25 -0700"), "Date does not match."); + Assert.That (envelope.Subject, Is.EqualTo ("IMAP4rev1 WG mtg summary and minutes"), "Subject does not match."); + + Assert.That (envelope.From, Has.Count.EqualTo (1), "From counts do not match."); + Assert.That (envelope.From.ToString (), Is.EqualTo ("\"Terry Gray\" "), "From does not match."); + + Assert.That (envelope.Sender, Has.Count.EqualTo (1), "Sender counts do not match."); + Assert.That (envelope.Sender.ToString (), Is.EqualTo ("\"Terry Gray\" "), "Sender does not match."); + + Assert.That (envelope.ReplyTo, Has.Count.EqualTo (1), "Reply-To counts do not match."); + Assert.That (envelope.ReplyTo.ToString (), Is.EqualTo ("\"Terry Gray\" "), "Reply-To does not match."); + + Assert.That (envelope.To, Has.Count.EqualTo (1), "To counts do not match."); + Assert.That (envelope.To.ToString (), Is.EqualTo ("imap@cac.washington.edu"), "To does not match."); + + Assert.That (envelope.Cc, Has.Count.EqualTo (2), "Cc counts do not match."); + Assert.That (envelope.Cc.ToString (), Is.EqualTo ("minutes@CNRI.Reston.VA.US, \"John Klensin\" "), "Cc does not match."); + + Assert.That (envelope.Bcc, Is.Empty, "Bcc counts do not match."); + + Assert.That (envelope.InReplyTo, Is.Null, "In-Reply-To is not null."); + + Assert.That (envelope.MessageId, Is.EqualTo ("B27397-0100000@cac.washington.edu"), "Message-Id does not match."); + } + } + } + } + + // This tests the work-around for issue #1369 + [Test] + public void TestParseEnvelopeWithMiscalculatedLiteralMailboxName () + { + const string text = "(\"Thu, 29 Apr 2021 10:57:07 +0000\" \"=?utf-8?B?0J/QsNGA0LrQuNC90LMg0L3QsCDQlNCw0L3QsNC40Lsg0JTQtdGH0LXQsg==?=\" (({38}\r\nРецепция Офис сграда \"Данаил Дечев\" №6 NIL \"facility\" \"xxxxxxxxxxx.com\")) NIL NIL ((\"Team\" NIL \"team\" \"xxxxxxxxxxx.com\")) NIL NIL NIL \"\")\r\n"; + + // Note: The server appears to have calculated the literal length as the number of unicode *characters* as opposed to *bytes*. The actual literal length *should be* 69, not 38. + using (var memory = new MemoryStream (Encoding.UTF8.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + Envelope envelope; + + engine.SetStream (tokenizer); + + try { + envelope = ImapUtils.ParseEnvelope (engine, CancellationToken.None); } catch (Exception ex) { - Assert.Fail ("Parsing BODYSTRUCTURE failed: {0}", ex); + Assert.Fail ($"Parsing ENVELOPE failed: {ex}"); return; } var token = engine.ReadToken (CancellationToken.None); - Assert.AreEqual (ImapTokenType.Eoln, token.Type, "Expected new-line, but got: {0}", token); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); - Assert.IsInstanceOf (body, "Body types did not match."); - multipart = (BodyPartMultipart) body; + Assert.That (envelope.Date.HasValue, Is.True, "Parsed ENVELOPE date is null."); + Assert.That (DateUtils.FormatDate (envelope.Date.Value), Is.EqualTo ("Thu, 29 Apr 2021 10:57:07 +0000"), "Date does not match."); + Assert.That (envelope.Subject, Is.EqualTo ("Паркинг на Данаил Дечев"), "Subject does not match."); - Assert.IsTrue (body.ContentType.IsMimeType ("multipart", "mixed"), "Content-Type did not match."); - Assert.AreEqual ("----=_NextPart_000_0077_01CBB179.57530990", body.ContentType.Parameters["boundary"], "boundary param did not match"); - Assert.AreEqual (3, multipart.BodyParts.Count, "BodyParts count does not match."); - Assert.IsInstanceOf (multipart.BodyParts[0], "The type of the first child does not match."); - Assert.IsInstanceOf (multipart.BodyParts[1], "The type of the second child does not match."); - Assert.IsInstanceOf (multipart.BodyParts[2], "The type of the third child does not match."); + Assert.That (envelope.From, Has.Count.EqualTo (1), "From counts do not match."); + Assert.That (envelope.From.ToString (), Is.EqualTo ("\"Рецепция Офис сграда \\\"Данаил Дечев\\\" №6\" "), "From does not match."); - // FIXME: assert more stuff? + Assert.That (envelope.Sender, Is.Empty, "Sender counts do not match."); + Assert.That (envelope.ReplyTo, Is.Empty, "Reply-To counts do not match."); + + Assert.That (envelope.To, Has.Count.EqualTo (1), "To counts do not match."); + Assert.That (envelope.To.ToString (), Is.EqualTo ("\"Team\" "), "To does not match."); + + Assert.That (envelope.Cc, Is.Empty, "Cc counts do not match."); + Assert.That (envelope.Bcc, Is.Empty, "Bcc counts do not match."); + + Assert.That (envelope.InReplyTo, Is.Null, "In-Reply-To is not null."); + + Assert.That (envelope.MessageId, Is.EqualTo ("d0f6ca6608cfb0b680b7b90824c79118@xxxxxxxxxxx.com"), "Message-Id does not match."); } } } } - // Note: This tests the work-around for issue #485 + // This tests the work-around for issue #1369 [Test] - public void TestParseBadlyQuotedBodyStructure () + public async Task TestParseEnvelopeWithMiscalculatedLiteralMailboxNameAsync () { - const string text = "((\"MOUNDARY=\"_006_5DBB50A5A54730AD4A54730AD4A54730AD4A54730AD42KOS_\"\" \"OCTET-STREAM\" (\"name\" \"test.dat\") NIL NIL \"quoted-printable\" 383137 NIL (\"attachment\" (\"filename\" \"test.dat\")))(\"MOUNDARY=\"_006_5DBB50A5D3ABEC4E85A03EAD527CA5474B3D0AF9E6EXMBXSVR02KOS_\"\" \"OCTET-STREAM\" (\"name\" \"test.dat\") NIL NIL \"quoted-printable\" 383137 NIL (\"attachment\" (\"filename\" \"test.dat\"))) \"MIXED\" (\"boundary\" \"----=_NextPart_000_730AD4A547.730AD4A547F40\"))\r\n"; + const string text = "(\"Thu, 29 Apr 2021 10:57:07 +0000\" \"=?utf-8?B?0J/QsNGA0LrQuNC90LMg0L3QsCDQlNCw0L3QsNC40Lsg0JTQtdGH0LXQsg==?=\" (({38}\r\nРецепция Офис сграда \"Данаил Дечев\" №6 NIL \"facility\" \"xxxxxxxxxxx.com\")) NIL NIL ((\"Team\" NIL \"team\" \"xxxxxxxxxxx.com\")) NIL NIL NIL \"\")\r\n"; + + // Note: The server appears to have calculated the literal length as the number of unicode *characters* as opposed to *bytes*. The actual literal length *should be* 69, not 38. + using (var memory = new MemoryStream (Encoding.UTF8.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + Envelope envelope; + + engine.SetStream (tokenizer); + + try { + envelope = await ImapUtils.ParseEnvelopeAsync (engine, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing ENVELOPE failed: {ex}"); + return; + } + + var token = await engine.ReadTokenAsync (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (envelope.Date.HasValue, Is.True, "Parsed ENVELOPE date is null."); + Assert.That (DateUtils.FormatDate (envelope.Date.Value), Is.EqualTo ("Thu, 29 Apr 2021 10:57:07 +0000"), "Date does not match."); + Assert.That (envelope.Subject, Is.EqualTo ("Паркинг на Данаил Дечев"), "Subject does not match."); + + Assert.That (envelope.From, Has.Count.EqualTo (1), "From counts do not match."); + Assert.That (envelope.From.ToString (), Is.EqualTo ("\"Рецепция Офис сграда \\\"Данаил Дечев\\\" №6\" "), "From does not match."); + + Assert.That (envelope.Sender, Is.Empty, "Sender counts do not match."); + Assert.That (envelope.ReplyTo, Is.Empty, "Reply-To counts do not match."); + + Assert.That (envelope.To, Has.Count.EqualTo (1), "To counts do not match."); + Assert.That (envelope.To.ToString (), Is.EqualTo ("\"Team\" "), "To does not match."); + + Assert.That (envelope.Cc, Is.Empty, "Cc counts do not match."); + Assert.That (envelope.Bcc, Is.Empty, "Bcc counts do not match."); + + Assert.That (envelope.InReplyTo, Is.Null, "In-Reply-To is not null."); + + Assert.That (envelope.MessageId, Is.EqualTo ("d0f6ca6608cfb0b680b7b90824c79118@xxxxxxxxxxx.com"), "Message-Id does not match."); + } + } + } + } + + // This tests the work-around for issue #669 + [Test] + public void TestParseEnvelopeWithMissingMessageId () + { + const string text = "(\"Tue, 24 Sep 2019 09:48:05 +0800\" \"subject\" ((\"From Name\" NIL \"from\" \"example.com\")) ((\"Sender Name\" NIL \"sender\" \"example.com\")) ((\"Reply-To Name\" NIL \"reply-to\" \"example.com\")) NIL NIL NIL \"\")\r\n"; using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { - using (var tokenizer = new ImapStream (memory, null, new NullProtocolLogger ())) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { using (var engine = new ImapEngine (null)) { - BodyPartMultipart multipart; - BodyPartBasic basic; - BodyPart body; + Envelope envelope; engine.SetStream (tokenizer); try { - body = ImapUtils.ParseBody (engine, "Unexpected token: {0}", string.Empty, CancellationToken.None); + envelope = ImapUtils.ParseEnvelope (engine, CancellationToken.None); } catch (Exception ex) { - Assert.Fail ("Parsing BODYSTRUCTURE failed: {0}", ex); + Assert.Fail ($"Parsing ENVELOPE failed: {ex}"); return; } var token = engine.ReadToken (CancellationToken.None); - Assert.AreEqual (ImapTokenType.Eoln, token.Type, "Expected new-line, but got: {0}", token); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); - Assert.IsInstanceOf (body, "Body types did not match."); - multipart = (BodyPartMultipart) body; + Assert.That (envelope.Date.HasValue, Is.True, "Parsed ENVELOPE date is null."); + Assert.That (DateUtils.FormatDate (envelope.Date.Value), Is.EqualTo ("Tue, 24 Sep 2019 09:48:05 +0800"), "Date does not match."); + Assert.That (envelope.Subject, Is.EqualTo ("subject"), "Subject does not match."); - Assert.IsTrue (body.ContentType.IsMimeType ("multipart", "mixed"), "Content-Type did not match."); - Assert.AreEqual ("----=_NextPart_000_730AD4A547.730AD4A547F40", body.ContentType.Parameters ["boundary"], "boundary param did not match"); - Assert.AreEqual (2, multipart.BodyParts.Count, "BodyParts count does not match."); + Assert.That (envelope.From, Has.Count.EqualTo (1), "From counts do not match."); + Assert.That (envelope.From.ToString (), Is.EqualTo ("\"From Name\" "), "From does not match."); - Assert.IsInstanceOf (multipart.BodyParts[0], "The type of the first child does not match."); - basic = (BodyPartBasic) multipart.BodyParts[0]; - Assert.AreEqual ("MOUNDARY=\"_006_5DBB50A5A54730AD4A54730AD4A54730AD4A54730AD42KOS_\"", basic.ContentType.MediaType, "ContentType.MediaType does not match for first child."); + Assert.That (envelope.Sender, Has.Count.EqualTo (1), "Sender counts do not match."); + Assert.That (envelope.Sender.ToString (), Is.EqualTo ("\"Sender Name\" "), "Sender does not match."); - Assert.IsInstanceOf (multipart.BodyParts[1], "The type of the second child does not match."); - basic = (BodyPartBasic) multipart.BodyParts[1]; - Assert.AreEqual ("MOUNDARY=\"_006_5DBB50A5D3ABEC4E85A03EAD527CA5474B3D0AF9E6EXMBXSVR02KOS_\"", basic.ContentType.MediaType, "ContentType.MediaType does not match for second child."); + Assert.That (envelope.ReplyTo, Has.Count.EqualTo (1), "Reply-To counts do not match."); + Assert.That (envelope.ReplyTo.ToString (), Is.EqualTo ("\"Reply-To Name\" "), "Reply-To does not match."); + + Assert.That (envelope.To, Is.Empty, "To counts do not match."); + Assert.That (envelope.Cc, Is.Empty, "Cc counts do not match."); + Assert.That (envelope.Bcc, Is.Empty, "Bcc counts do not match."); + + Assert.That (envelope.InReplyTo, Is.EqualTo ("in-reply-to@example.com"), "In-Reply-To does not match."); + + Assert.That (envelope.MessageId, Is.Null, "Message-Id is not null."); } } } } + // This tests the work-around for issue #669 [Test] - public void TestParseMultipartBodyStructureWithNilBodyFldParam () + public async Task TestParseEnvelopeWithMissingMessageIdAsync () { - const string text = "(((\"text\" \"plain\" (\"charset\" \"UTF-8\") NIL NIL \"7bit\" 148 12 NIL NIL NIL NIL)(\"text\" \"html\" (\"charset\" \"UTF-8\") NIL NIL \"quoted-printable\" 337 6 NIL NIL NIL NIL) \"alternative\" (\"boundary\" \"6c7f221bed92d80548353834d8e2\") NIL NIL NIL)((\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 0 0) \"x-zip\" NIL (\"attachment\" (\"filename\" \"YSOZ 265230.ZIP\")) NIL NIL) \"mixed\" (\"boundary\" \"c52bbfc0dd5365efa39b9f80eac3\") NIL NIL NIL)\r\n"; + const string text = "(\"Tue, 24 Sep 2019 09:48:05 +0800\" \"subject\" ((\"From Name\" NIL \"from\" \"example.com\")) ((\"Sender Name\" NIL \"sender\" \"example.com\")) ((\"Reply-To Name\" NIL \"reply-to\" \"example.com\")) NIL NIL NIL \"\")\r\n"; using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { - using (var tokenizer = new ImapStream (memory, null, new NullProtocolLogger ())) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { using (var engine = new ImapEngine (null)) { - BodyPartMultipart multipart, alternative, xzip; - BodyPart body; + Envelope envelope; engine.SetStream (tokenizer); try { - body = ImapUtils.ParseBody (engine, "Unexpected token: {0}", string.Empty, CancellationToken.None); + envelope = await ImapUtils.ParseEnvelopeAsync (engine, CancellationToken.None); } catch (Exception ex) { - Assert.Fail ("Parsing BODYSTRUCTURE failed: {0}", ex); + Assert.Fail ($"Parsing ENVELOPE failed: {ex}"); return; } - var token = engine.ReadToken (CancellationToken.None); - Assert.AreEqual (ImapTokenType.Eoln, token.Type, "Expected new-line, but got: {0}", token); + var token = await engine.ReadTokenAsync (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); - Assert.IsInstanceOf (body, "Body types did not match."); - multipart = (BodyPartMultipart) body; + Assert.That (envelope.Date.HasValue, Is.True, "Parsed ENVELOPE date is null."); + Assert.That (DateUtils.FormatDate (envelope.Date.Value), Is.EqualTo ("Tue, 24 Sep 2019 09:48:05 +0800"), "Date does not match."); + Assert.That (envelope.Subject, Is.EqualTo ("subject"), "Subject does not match."); - Assert.IsTrue (body.ContentType.IsMimeType ("multipart", "mixed"), "Content-Type did not match."); - Assert.AreEqual ("c52bbfc0dd5365efa39b9f80eac3", body.ContentType.Parameters["boundary"], "boundary param did not match"); - Assert.AreEqual (2, multipart.BodyParts.Count, "BodyParts count does not match."); + Assert.That (envelope.From, Has.Count.EqualTo (1), "From counts do not match."); + Assert.That (envelope.From.ToString (), Is.EqualTo ("\"From Name\" "), "From does not match."); - Assert.IsInstanceOf (multipart.BodyParts[0], "The type of the first child does not match."); - alternative = (BodyPartMultipart) multipart.BodyParts[0]; - Assert.AreEqual ("alternative", alternative.ContentType.MediaSubtype, "Content-Type did not match."); + Assert.That (envelope.Sender, Has.Count.EqualTo (1), "Sender counts do not match."); + Assert.That (envelope.Sender.ToString (), Is.EqualTo ("\"Sender Name\" "), "Sender does not match."); - Assert.IsInstanceOf (multipart.BodyParts[1], "The type of the second child does not match."); - xzip = (BodyPartMultipart) multipart.BodyParts[1]; - Assert.AreEqual ("x-zip", xzip.ContentType.MediaSubtype, "Content-Type did not match."); - Assert.AreEqual (0, xzip.ContentType.Parameters.Count, "Content-Type should not have params."); + Assert.That (envelope.ReplyTo, Has.Count.EqualTo (1), "Reply-To counts do not match."); + Assert.That (envelope.ReplyTo.ToString (), Is.EqualTo ("\"Reply-To Name\" "), "Reply-To does not match."); + + Assert.That (envelope.To, Is.Empty, "To counts do not match."); + Assert.That (envelope.Cc, Is.Empty, "Cc counts do not match."); + Assert.That (envelope.Bcc, Is.Empty, "Bcc counts do not match."); + + Assert.That (envelope.InReplyTo, Is.EqualTo ("in-reply-to@example.com"), "In-Reply-To does not match."); + + Assert.That (envelope.MessageId, Is.Null, "Message-Id is not null."); } } } } + // This tests the work-around for issue #932 [Test] - public void TestParseExampleThreads () + public void TestParseEnvelopeWithMissingInReplyTo () { - const string text = "(2)(3 6 (4 23)(44 7 96))\r\n"; + const string text = "(\"Tue, 24 Sep 2019 09:48:05 +0800\" \"=?GBK?B?sbG+qdW9x/jI1bGose0=?=\" ((\"=?GBK?B?yv2+3bfWzvbQodfp?=\" NIL \"unknown-name\" \"unknown-domain\")) ((\"=?GBK?B?yv2+3bfWzvbQodfp?=\" NIL \"unknown-name\" \"unknown-domain\")) ((\"=?GBK?B?yv2+3bfWzvbQodfp?=\" NIL \"unknown-name\" \"unknown-domain\")) NIL NIL NIL)\r\n"; using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { - using (var tokenizer = new ImapStream (memory, null, new NullProtocolLogger ())) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { using (var engine = new ImapEngine (null)) { - IList threads; + Envelope envelope; engine.SetStream (tokenizer); try { - threads = ImapUtils.ParseThreads (engine, 0, CancellationToken.None); + envelope = ImapUtils.ParseEnvelope (engine, CancellationToken.None); } catch (Exception ex) { - Assert.Fail ("Parsing THREAD response failed: {0}", ex); + Assert.Fail ($"Parsing ENVELOPE failed: {ex}"); return; } var token = engine.ReadToken (CancellationToken.None); - Assert.AreEqual (ImapTokenType.Eoln, token.Type, "Expected new-line, but got: {0}", token); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); - Assert.AreEqual (2, threads.Count, "Expected 2 threads."); + Assert.That (envelope.Date.HasValue, Is.True, "Parsed ENVELOPE date is null."); + Assert.That (DateUtils.FormatDate (envelope.Date.Value), Is.EqualTo ("Tue, 24 Sep 2019 09:48:05 +0800"), "Date does not match."); + Assert.That (envelope.Subject, Is.EqualTo ("北京战区日报表"), "Subject does not match."); - Assert.AreEqual ((uint) 2, threads[0].UniqueId.Value.Id); - Assert.AreEqual ((uint) 3, threads[1].UniqueId.Value.Id); + Assert.That (envelope.From, Has.Count.EqualTo (1), "From counts do not match."); + Assert.That (envelope.From.ToString (), Is.EqualTo ("\"数据分析小组\" "), "From does not match."); - var branches = threads[1].Children.ToArray (); - Assert.AreEqual (1, branches.Length, "Expected 1 child."); - Assert.AreEqual ((uint) 6, branches[0].UniqueId.Value.Id); + Assert.That (envelope.Sender, Has.Count.EqualTo (1), "Sender counts do not match."); + Assert.That (envelope.Sender.ToString (), Is.EqualTo ("\"数据分析小组\" "), "Sender does not match."); - branches = branches[0].Children.ToArray (); - Assert.AreEqual (2, branches.Length, "Expected 2 branches."); + Assert.That (envelope.ReplyTo, Has.Count.EqualTo (1), "Reply-To counts do not match."); + Assert.That (envelope.ReplyTo.ToString (), Is.EqualTo ("\"数据分析小组\" "), "Reply-To does not match."); - Assert.AreEqual ((uint) 4, branches[0].UniqueId.Value.Id); - Assert.AreEqual ((uint) 44, branches[1].UniqueId.Value.Id); + Assert.That (envelope.To, Is.Empty, "To counts do not match."); + Assert.That (envelope.Cc, Is.Empty, "Cc counts do not match."); + Assert.That (envelope.Bcc, Is.Empty, "Bcc counts do not match."); - var children = branches[0].Children.ToArray (); - Assert.AreEqual (1, children.Length, "Expected 1 child."); - Assert.AreEqual ((uint) 23, children[0].UniqueId.Value.Id); - Assert.AreEqual (0, children[0].Children.Count (), "Expected no children."); + Assert.That (envelope.InReplyTo, Is.Null, "In-Reply-To is not null."); + Assert.That (envelope.MessageId, Is.Null, "Message-Id is not null."); + } + } + } + } - children = branches[1].Children.ToArray (); - Assert.AreEqual (1, children.Length, "Expected 1 child."); - Assert.AreEqual ((uint) 7, children[0].UniqueId.Value.Id); + // This tests the work-around for issue #932 + [Test] + public async Task TestParseEnvelopeWithMissingInReplyToAsync () + { + const string text = "(\"Tue, 24 Sep 2019 09:48:05 +0800\" \"=?GBK?B?sbG+qdW9x/jI1bGose0=?=\" ((\"=?GBK?B?yv2+3bfWzvbQodfp?=\" NIL \"unknown-name\" \"unknown-domain\")) ((\"=?GBK?B?yv2+3bfWzvbQodfp?=\" NIL \"unknown-name\" \"unknown-domain\")) ((\"=?GBK?B?yv2+3bfWzvbQodfp?=\" NIL \"unknown-name\" \"unknown-domain\")) NIL NIL NIL)\r\n"; - children = children[0].Children.ToArray (); - Assert.AreEqual (1, children.Length, "Expected 1 child."); - Assert.AreEqual ((uint) 96, children[0].UniqueId.Value.Id); - Assert.AreEqual (0, children[0].Children.Count (), "Expected no children."); + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + Envelope envelope; + + engine.SetStream (tokenizer); + + try { + envelope = await ImapUtils.ParseEnvelopeAsync (engine, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing ENVELOPE failed: {ex}"); + return; + } + + var token = await engine.ReadTokenAsync (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (envelope.Date.HasValue, Is.True, "Parsed ENVELOPE date is null."); + Assert.That (DateUtils.FormatDate (envelope.Date.Value), Is.EqualTo ("Tue, 24 Sep 2019 09:48:05 +0800"), "Date does not match."); + Assert.That (envelope.Subject, Is.EqualTo ("北京战区日报表"), "Subject does not match."); + + Assert.That (envelope.From, Has.Count.EqualTo (1), "From counts do not match."); + Assert.That (envelope.From.ToString (), Is.EqualTo ("\"数据分析小组\" "), "From does not match."); + + Assert.That (envelope.Sender, Has.Count.EqualTo (1), "Sender counts do not match."); + Assert.That (envelope.Sender.ToString (), Is.EqualTo ("\"数据分析小组\" "), "Sender does not match."); + + Assert.That (envelope.ReplyTo, Has.Count.EqualTo (1), "Reply-To counts do not match."); + Assert.That (envelope.ReplyTo.ToString (), Is.EqualTo ("\"数据分析小组\" "), "Reply-To does not match."); + + Assert.That (envelope.To, Is.Empty, "To counts do not match."); + Assert.That (envelope.Cc, Is.Empty, "Cc counts do not match."); + Assert.That (envelope.Bcc, Is.Empty, "Bcc counts do not match."); + + Assert.That (envelope.InReplyTo, Is.Null, "In-Reply-To is not null."); + Assert.That (envelope.MessageId, Is.Null, "Message-Id is not null."); } } } } + + [Test] + public void TestParseMalformedMailboxAddressInEnvelope () + { + const string text = "(\"Mon, 10 Apr 2017 06:04:00 -0700\" \"Session 2: Building the meditation habit\" ((\"Headspace\" NIL \"members\" \"headspace.com\")) ((NIL NIL \"\")) ((\"Headspace\" NIL \"members\" \"headspace.com\")) ((NIL NIL \"user\" \"gmail.com\")) NIL NIL NIL \"\")"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + Envelope envelope; + + engine.SetStream (tokenizer); + + try { + envelope = ImapUtils.ParseEnvelope (engine, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing ENVELOPE failed: {ex}"); + return; + } + + Assert.That (envelope.Date.HasValue, Is.True, "Parsed ENVELOPE date is null."); + Assert.That (DateUtils.FormatDate (envelope.Date.Value), Is.EqualTo ("Mon, 10 Apr 2017 06:04:00 -0700"), "Date does not match."); + Assert.That (envelope.Subject, Is.EqualTo ("Session 2: Building the meditation habit"), "Subject does not match."); + + Assert.That (envelope.From, Has.Count.EqualTo (1), "From counts do not match."); + Assert.That (envelope.From.ToString (), Is.EqualTo ("\"Headspace\" "), "From does not match."); + + Assert.That (envelope.Sender, Has.Count.EqualTo (1), "Sender counts do not match."); + Assert.That (envelope.Sender.ToString (), Is.EqualTo ("members=headspace.com@members.headspace.com"), "Sender does not match."); + + Assert.That (envelope.ReplyTo, Has.Count.EqualTo (1), "Reply-To counts do not match."); + Assert.That (envelope.ReplyTo.ToString (), Is.EqualTo ("\"Headspace\" "), "Reply-To does not match."); + + Assert.That (envelope.To, Has.Count.EqualTo (1), "To counts do not match."); + Assert.That (envelope.To.ToString (), Is.EqualTo ("user@gmail.com"), "To does not match."); + + Assert.That (envelope.Cc, Is.Empty, "Cc counts do not match."); + Assert.That (envelope.Bcc, Is.Empty, "Bcc counts do not match."); + + Assert.That (envelope.InReplyTo, Is.Null, "In-Reply-To is not null."); + + Assert.That (envelope.MessageId, Is.EqualTo ("bvqyalstpemxt9y3afoqh4an62b2arcd.rcd.1491829440@members.headspace.com"), "Message-Id does not match."); + } + } + } + } + + [Test] + public async Task TestParseMalformedMailboxAddressInEnvelopeAsync () + { + const string text = "(\"Mon, 10 Apr 2017 06:04:00 -0700\" \"Session 2: Building the meditation habit\" ((\"Headspace\" NIL \"members\" \"headspace.com\")) ((NIL NIL \"\")) ((\"Headspace\" NIL \"members\" \"headspace.com\")) ((NIL NIL \"user\" \"gmail.com\")) NIL NIL NIL \"\")"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + Envelope envelope; + + engine.SetStream (tokenizer); + + try { + envelope = await ImapUtils.ParseEnvelopeAsync (engine, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing ENVELOPE failed: {ex}"); + return; + } + + Assert.That (envelope.Date.HasValue, Is.True, "Parsed ENVELOPE date is null."); + Assert.That (DateUtils.FormatDate (envelope.Date.Value), Is.EqualTo ("Mon, 10 Apr 2017 06:04:00 -0700"), "Date does not match."); + Assert.That (envelope.Subject, Is.EqualTo ("Session 2: Building the meditation habit"), "Subject does not match."); + + Assert.That (envelope.From, Has.Count.EqualTo (1), "From counts do not match."); + Assert.That (envelope.From.ToString (), Is.EqualTo ("\"Headspace\" "), "From does not match."); + + Assert.That (envelope.Sender, Has.Count.EqualTo (1), "Sender counts do not match."); + Assert.That (envelope.Sender.ToString (), Is.EqualTo ("members=headspace.com@members.headspace.com"), "Sender does not match."); + + Assert.That (envelope.ReplyTo, Has.Count.EqualTo (1), "Reply-To counts do not match."); + Assert.That (envelope.ReplyTo.ToString (), Is.EqualTo ("\"Headspace\" "), "Reply-To does not match."); + + Assert.That (envelope.To, Has.Count.EqualTo (1), "To counts do not match."); + Assert.That (envelope.To.ToString (), Is.EqualTo ("user@gmail.com"), "To does not match."); + + Assert.That (envelope.Cc, Is.Empty, "Cc counts do not match."); + Assert.That (envelope.Bcc, Is.Empty, "Bcc counts do not match."); + + Assert.That (envelope.InReplyTo, Is.Null, "In-Reply-To is not null."); + + Assert.That (envelope.MessageId, Is.EqualTo ("bvqyalstpemxt9y3afoqh4an62b2arcd.rcd.1491829440@members.headspace.com"), "Message-Id does not match."); + } + } + } + } + + // This tests a work-around for a bug in Gmail in which the sender header is in a correct format + // (Sender: Name ) but in the FETCH response is not ((("" NIL "Name" NIL))) + [Test] + public void TestParseGMailMalformedSenderInEnvelope () + { + const string text = "(\"Mon, 10 Apr 2017 06:04:00 -0700\" \"This is the subject\" ((\"From_DisplayName\" NIL \"from\" \"domain.com\")) ((\"\" NIL \"=?UTF-8?Q?\"Dummy=C3=ADa_Pa=C3=A1ndez_Algo\"?=\" NIL)) NIL ((\"To_DisplayName\" NIL \"to\" \"domain.com\")) NIL NIL NIL \"\")"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + Envelope envelope; + + engine.SetStream (tokenizer); + engine.QuirksMode = ImapQuirksMode.GMail; + + try { + envelope = ImapUtils.ParseEnvelope (engine, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing ENVELOPE failed: {ex}"); + return; + } + + Assert.That (envelope.Date.HasValue, Is.True, "Parsed ENVELOPE date is null."); + Assert.That (DateUtils.FormatDate (envelope.Date.Value), Is.EqualTo ("Mon, 10 Apr 2017 06:04:00 -0700"), "Date does not match."); + Assert.That (envelope.Subject, Is.EqualTo ("This is the subject"), "Subject does not match."); + + Assert.That (envelope.From, Has.Count.EqualTo (1), "From counts do not match."); + Assert.That (envelope.From.ToString (), Is.EqualTo ("\"From_DisplayName\" "), "From does not match."); + + Assert.That (envelope.Sender, Has.Count.EqualTo (1), "Sender counts do not match."); + Assert.That (envelope.Sender.ToString (), Is.EqualTo ("\"Dummyía Paández Algo\" "), "Sender does not match."); + + Assert.That (envelope.To, Has.Count.EqualTo (1), "To counts do not match."); + Assert.That (envelope.To.ToString (), Is.EqualTo ("\"To_DisplayName\" "), "To does not match."); + + Assert.That (envelope.ReplyTo, Is.Empty, "Reply-To counts do not match."); + Assert.That (envelope.Cc, Is.Empty, "Cc counts do not match."); + Assert.That (envelope.Bcc, Is.Empty, "Bcc counts do not match."); + + Assert.That (envelope.InReplyTo, Is.Null, "In-Reply-To is not null."); + + Assert.That (envelope.MessageId, Is.EqualTo ("bvqyalstpemxt9y3afoqh4an62b2arcd@message.id"), "Message-Id does not match."); + } + } + } + } + + // This tests a work-around for a bug in Gmail in which the sender header is in a correct format + // (Sender: Name ) but in the FETCH response is not ((("" NIL "Name" NIL))) + [Test] + public async Task TestParseGMailMalformedSenderInEnvelopeAsync () + { + const string text = "(\"Mon, 10 Apr 2017 06:04:00 -0700\" \"This is the subject\" ((\"From_DisplayName\" NIL \"from\" \"domain.com\")) ((\"\" NIL \"=?UTF-8?Q?\"Dummy=C3=ADa_Pa=C3=A1ndez_Algo\"?=\" NIL)) NIL ((\"To_DisplayName\" NIL \"to\" \"domain.com\")) NIL NIL NIL \"\")"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + Envelope envelope; + + engine.SetStream (tokenizer); + engine.QuirksMode = ImapQuirksMode.GMail; + + try { + envelope = await ImapUtils.ParseEnvelopeAsync (engine, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing ENVELOPE failed: {ex}"); + return; + } + + Assert.That (envelope.Date.HasValue, Is.True, "Parsed ENVELOPE date is null."); + Assert.That (DateUtils.FormatDate (envelope.Date.Value), Is.EqualTo ("Mon, 10 Apr 2017 06:04:00 -0700"), "Date does not match."); + Assert.That (envelope.Subject, Is.EqualTo ("This is the subject"), "Subject does not match."); + + Assert.That (envelope.From, Has.Count.EqualTo (1), "From counts do not match."); + Assert.That (envelope.From.ToString (), Is.EqualTo ("\"From_DisplayName\" "), "From does not match."); + + Assert.That (envelope.Sender, Has.Count.EqualTo (1), "Sender counts do not match."); + Assert.That (envelope.Sender.ToString (), Is.EqualTo ("\"Dummyía Paández Algo\" "), "Sender does not match."); + + Assert.That (envelope.To, Has.Count.EqualTo (1), "To counts do not match."); + Assert.That (envelope.To.ToString (), Is.EqualTo ("\"To_DisplayName\" "), "To does not match."); + + Assert.That (envelope.ReplyTo, Is.Empty, "Reply-To counts do not match."); + Assert.That (envelope.Cc, Is.Empty, "Cc counts do not match."); + Assert.That (envelope.Bcc, Is.Empty, "Bcc counts do not match."); + + Assert.That (envelope.InReplyTo, Is.Null, "In-Reply-To is not null."); + + Assert.That (envelope.MessageId, Is.EqualTo ("bvqyalstpemxt9y3afoqh4an62b2arcd@message.id"), "Message-Id does not match."); + } + } + } + } + + // This tests issue #1451 + [Test] + public void TestParseEnvelopeWithNilMailbox () + { + const string text = "(NIL \"Retrieval using the IMAP4 protocol failed for the following message: 3\" ((\"Microsoft Exchange Server\" NIL NIL \".MISSING-HOST-NAME.\")) NIL NIL ((\"username@testdomain.com\" NIL \"username\" \"testdomain.com\")) NIL NIL NIL NIL)"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + Envelope envelope; + + engine.SetStream (tokenizer); + + try { + envelope = ImapUtils.ParseEnvelope (engine, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing ENVELOPE failed: {ex}"); + return; + } + + Assert.That (envelope.Subject, Is.EqualTo ("Retrieval using the IMAP4 protocol failed for the following message: 3")); + Assert.That (envelope.From.ToString (), Is.EqualTo ("\"Microsoft Exchange Server\" <>")); + Assert.That (envelope.To.ToString (), Is.EqualTo ("\"username@testdomain.com\" ")); + } + } + } + } + + [Test] + public async Task TestParseEnvelopeWithNilMailboxAsync () + { + const string text = "(NIL \"Retrieval using the IMAP4 protocol failed for the following message: 3\" ((\"Microsoft Exchange Server\" NIL NIL \".MISSING-HOST-NAME.\")) NIL NIL ((\"username@testdomain.com\" NIL \"username\" \"testdomain.com\")) NIL NIL NIL NIL)"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + Envelope envelope; + + engine.SetStream (tokenizer); + + try { + envelope = await ImapUtils.ParseEnvelopeAsync (engine, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing ENVELOPE failed: {ex}"); + return; + } + + Assert.That (envelope.Subject, Is.EqualTo ("Retrieval using the IMAP4 protocol failed for the following message: 3")); + Assert.That (envelope.From.ToString (), Is.EqualTo ("\"Microsoft Exchange Server\" <>")); + Assert.That (envelope.To.ToString (), Is.EqualTo ("\"username@testdomain.com\" ")); + } + } + } + } + + [Test] + public void TestParseEnvelopeWithRoutedMailboxes () + { + const string text = "(\"Mon, 13 Jul 2015 21:15:32 -0400\" \"Test message\" ((\"Example From\" \"@route1,@route2\" \"from\" \"example.com\")) ((\"Example Sender\" NIL \"sender\" \"example.com\")) ((\"Example Reply-To\" NIL \"reply-to\" \"example.com\")) ((NIL NIL \"boys\" NIL)(NIL NIL \"aaron\" \"MISSING_DOMAIN\")(NIL NIL \"jeff\" \"MISSING_DOMAIN\")(NIL NIL \"zach\" \"MISSING_DOMAIN\")(NIL NIL NIL NIL)(NIL NIL \"girls\" NIL)(NIL NIL \"alice\" \"MISSING_DOMAIN\")(NIL NIL \"hailey\" \"MISSING_DOMAIN\")(NIL NIL \"jenny\" \"MISSING_DOMAIN\")(NIL NIL NIL NIL)) NIL NIL NIL \"\")"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + Envelope envelope; + + engine.SetStream (tokenizer); + + try { + envelope = ImapUtils.ParseEnvelope (engine, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing ENVELOPE failed: {ex}"); + return; + } + + Assert.That (envelope.Sender.ToString (), Is.EqualTo ("\"Example Sender\" ")); + Assert.That (envelope.From.ToString (), Is.EqualTo ("\"Example From\" <@route1,@route2:from@example.com>")); + Assert.That (envelope.ReplyTo.ToString (), Is.EqualTo ("\"Example Reply-To\" ")); + Assert.That (envelope.To.ToString (), Is.EqualTo ("boys: aaron, jeff, zach;, girls: alice, hailey, jenny;")); + } + } + } + } + + [Test] + public async Task TestParseEnvelopeWithRoutedMailboxesAsync () + { + const string text = "(\"Mon, 13 Jul 2015 21:15:32 -0400\" \"Test message\" ((\"Example From\" \"@route1,@route2\" \"from\" \"example.com\")) ((\"Example Sender\" NIL \"sender\" \"example.com\")) ((\"Example Reply-To\" NIL \"reply-to\" \"example.com\")) ((NIL NIL \"boys\" NIL)(NIL NIL \"aaron\" \"MISSING_DOMAIN\")(NIL NIL \"jeff\" \"MISSING_DOMAIN\")(NIL NIL \"zach\" \"MISSING_DOMAIN\")(NIL NIL NIL NIL)(NIL NIL \"girls\" NIL)(NIL NIL \"alice\" \"MISSING_DOMAIN\")(NIL NIL \"hailey\" \"MISSING_DOMAIN\")(NIL NIL \"jenny\" \"MISSING_DOMAIN\")(NIL NIL NIL NIL)) NIL NIL NIL \"\")"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + Envelope envelope; + + engine.SetStream (tokenizer); + + try { + envelope = await ImapUtils.ParseEnvelopeAsync (engine, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing ENVELOPE failed: {ex}"); + return; + } + + Assert.That (envelope.Sender.ToString (), Is.EqualTo ("\"Example Sender\" ")); + Assert.That (envelope.From.ToString (), Is.EqualTo ("\"Example From\" <@route1,@route2:from@example.com>")); + Assert.That (envelope.ReplyTo.ToString (), Is.EqualTo ("\"Example Reply-To\" ")); + Assert.That (envelope.To.ToString (), Is.EqualTo ("boys: aaron, jeff, zach;, girls: alice, hailey, jenny;")); + } + } + } + } + + // This tests the work-around for issue #991 + [Test] + public void TestParseEnvelopeWithNilAddress () + { + const string text = "(\"Thu, 18 Jul 2019 01:29:32 -0300\" \"Xxx xxx xxx xxx..\" (NIL ({123}\r\n_XXXXXXXX_xxxxxx_xxxx_xxx_?= =?iso-8859-1?Q?xxxx_xx_xxxxxxx_xxxxxxxxxx.Xxxxxxxx_xx_xxx=Xxxxxx_xx_xx_Xx?= =?iso-8859-1?Q?s?= NIL \"xxxxxxx\" \"xxxxxxxxxx.xxx\")) (NIL ({123}\r\n_XXXXXXXX_xxxxxx_xxxx_xxx_?= =?iso-8859-1?Q?xxxx_xx_xxxxxxx_xxxxxxxxxx.Xxxxxxxx_xx_xxx=Xxxxxx_xx_xx_Xx?= =?iso-8859-1?Q?s?= NIL \"xxxxxxx\" \"xxxxxxxxxx.xxx\")) ((NIL NIL \"xxxxxxx\" \"xxxxx.xxx.xx\")) ((NIL NIL \"xxxxxxx\" \"xxxxxxx.xxx.xx\")) NIL NIL NIL \"<0A9F01100712011D213C15B6D2B6DA@XXXXXXX-XXXXXXX>\"))\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + Envelope envelope; + + engine.SetStream (tokenizer); + + try { + envelope = ImapUtils.ParseEnvelope (engine, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing ENVELOPE failed: {ex}"); + return; + } + + Assert.That (envelope.Sender.ToString (), Is.EqualTo ("\"_XXXXXXXX_xxxxxx_xxxx_xxx_?= xxxx xx xxxxxxx xxxxxxxxxx.Xxxxxxxx xx xxx=Xxxxxx xx xx Xxs\" ")); + Assert.That (envelope.From.ToString (), Is.EqualTo ("\"_XXXXXXXX_xxxxxx_xxxx_xxx_?= xxxx xx xxxxxxx xxxxxxxxxx.Xxxxxxxx xx xxx=Xxxxxx xx xx Xxs\" ")); + Assert.That (envelope.ReplyTo.ToString (), Is.EqualTo ("xxxxxxx@xxxxx.xxx.xx")); + Assert.That (envelope.To.ToString (), Is.EqualTo ("xxxxxxx@xxxxxxx.xxx.xx")); + Assert.That (envelope.MessageId, Is.EqualTo ("0A9F01100712011D213C15B6D2B6DA@XXXXXXX-XXXXXXX")); + } + } + } + } + + // This tests the work-around for issue #991 + [Test] + public async Task TestParseEnvelopeWithNilAddressAsync () + { + const string text = "(\"Thu, 18 Jul 2019 01:29:32 -0300\" \"Xxx xxx xxx xxx..\" (NIL ({123}\r\n_XXXXXXXX_xxxxxx_xxxx_xxx_?= =?iso-8859-1?Q?xxxx_xx_xxxxxxx_xxxxxxxxxx.Xxxxxxxx_xx_xxx=Xxxxxx_xx_xx_Xx?= =?iso-8859-1?Q?s?= NIL \"xxxxxxx\" \"xxxxxxxxxx.xxx\")) (NIL ({123}\r\n_XXXXXXXX_xxxxxx_xxxx_xxx_?= =?iso-8859-1?Q?xxxx_xx_xxxxxxx_xxxxxxxxxx.Xxxxxxxx_xx_xxx=Xxxxxx_xx_xx_Xx?= =?iso-8859-1?Q?s?= NIL \"xxxxxxx\" \"xxxxxxxxxx.xxx\")) ((NIL NIL \"xxxxxxx\" \"xxxxx.xxx.xx\")) ((NIL NIL \"xxxxxxx\" \"xxxxxxx.xxx.xx\")) NIL NIL NIL \"<0A9F01100712011D213C15B6D2B6DA@XXXXXXX-XXXXXXX>\"))\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + Envelope envelope; + + engine.SetStream (tokenizer); + + try { + envelope = await ImapUtils.ParseEnvelopeAsync (engine, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing ENVELOPE failed: {ex}"); + return; + } + + Assert.That (envelope.Sender.ToString (), Is.EqualTo ("\"_XXXXXXXX_xxxxxx_xxxx_xxx_?= xxxx xx xxxxxxx xxxxxxxxxx.Xxxxxxxx xx xxx=Xxxxxx xx xx Xxs\" ")); + Assert.That (envelope.From.ToString (), Is.EqualTo ("\"_XXXXXXXX_xxxxxx_xxxx_xxx_?= xxxx xx xxxxxxx xxxxxxxxxx.Xxxxxxxx xx xxx=Xxxxxx xx xx Xxs\" ")); + Assert.That (envelope.ReplyTo.ToString (), Is.EqualTo ("xxxxxxx@xxxxx.xxx.xx")); + Assert.That (envelope.To.ToString (), Is.EqualTo ("xxxxxxx@xxxxxxx.xxx.xx")); + Assert.That (envelope.MessageId, Is.EqualTo ("0A9F01100712011D213C15B6D2B6DA@XXXXXXX-XXXXXXX")); + } + } + } + } + + [Test] + public void TestParseDovcotEnvelopeWithGroupAddresses () + { + const string text = "(\"Mon, 13 Jul 2015 21:15:32 -0400\" \"Test message\" ((\"Example From\" NIL \"from\" \"example.com\")) ((\"Example Sender\" NIL \"sender\" \"example.com\")) ((\"Example Reply-To\" NIL \"reply-to\" \"example.com\")) ((NIL NIL \"boys\" NIL)(NIL NIL \"aaron\" \"MISSING_DOMAIN\")(NIL NIL \"jeff\" \"MISSING_DOMAIN\")(NIL NIL \"zach\" \"MISSING_DOMAIN\")(NIL NIL NIL NIL)(NIL NIL \"girls\" NIL)(NIL NIL \"alice\" \"MISSING_DOMAIN\")(NIL NIL \"hailey\" \"MISSING_DOMAIN\")(NIL NIL \"jenny\" \"MISSING_DOMAIN\")(NIL NIL NIL NIL)) NIL NIL NIL \"\")"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + Envelope envelope; + + engine.SetStream (tokenizer); + + try { + envelope = ImapUtils.ParseEnvelope (engine, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing ENVELOPE failed: {ex}"); + return; + } + + Assert.That (envelope.Sender.ToString (), Is.EqualTo ("\"Example Sender\" ")); + Assert.That (envelope.From.ToString (), Is.EqualTo ("\"Example From\" ")); + Assert.That (envelope.ReplyTo.ToString (), Is.EqualTo ("\"Example Reply-To\" ")); + Assert.That (envelope.To.ToString (), Is.EqualTo ("boys: aaron, jeff, zach;, girls: alice, hailey, jenny;")); + } + } + } + } + + [Test] + public async Task TestParseDovcotEnvelopeWithGroupAddressesAsync () + { + const string text = "(\"Mon, 13 Jul 2015 21:15:32 -0400\" \"Test message\" ((\"Example From\" NIL \"from\" \"example.com\")) ((\"Example Sender\" NIL \"sender\" \"example.com\")) ((\"Example Reply-To\" NIL \"reply-to\" \"example.com\")) ((NIL NIL \"boys\" NIL)(NIL NIL \"aaron\" \"MISSING_DOMAIN\")(NIL NIL \"jeff\" \"MISSING_DOMAIN\")(NIL NIL \"zach\" \"MISSING_DOMAIN\")(NIL NIL NIL NIL)(NIL NIL \"girls\" NIL)(NIL NIL \"alice\" \"MISSING_DOMAIN\")(NIL NIL \"hailey\" \"MISSING_DOMAIN\")(NIL NIL \"jenny\" \"MISSING_DOMAIN\")(NIL NIL NIL NIL)) NIL NIL NIL \"\")"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + Envelope envelope; + + engine.SetStream (tokenizer); + + try { + envelope = await ImapUtils.ParseEnvelopeAsync (engine, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing ENVELOPE failed: {ex}"); + return; + } + + Assert.That (envelope.Sender.ToString (), Is.EqualTo ("\"Example Sender\" ")); + Assert.That (envelope.From.ToString (), Is.EqualTo ("\"Example From\" ")); + Assert.That (envelope.ReplyTo.ToString (), Is.EqualTo ("\"Example Reply-To\" ")); + Assert.That (envelope.To.ToString (), Is.EqualTo ("boys: aaron, jeff, zach;, girls: alice, hailey, jenny;")); + } + } + } + } + + [Test] + public void TestParseExampleMultiLevelDovecotBodyStructure () + { + const string text = "(((\"text\" \"plain\" (\"charset\" \"iso-8859-2\") NIL NIL \"quoted-printable\" 28 2 NIL NIL NIL NIL) (\"text\" \"html\" (\"charset\" \"iso-8859-2\") NIL NIL \"quoted-printable\" 1707 65 NIL NIL NIL NIL) \"alternative\" (\"boundary\" \"----=_NextPart_001_0078_01CBB179.57530990\") NIL NIL NIL) (\"message\" \"rfc822\" NIL NIL NIL \"7bit\" 641 (\"Sat, 8 Jan 2011 14:16:36 +0100\" \"Subj 2\" ((\"Some Name, SOMECOMPANY\" NIL \"recipient\" \"example.com\")) ((\"Some Name, SOMECOMPANY\" NIL \"recipient\" \"example.com\")) ((\"Some Name, SOMECOMPANY\" NIL \"recipient\" \"example.com\")) ((\"Recipient\" NIL \"example\" \"gmail.com\")) NIL NIL NIL NIL) (\"text\" \"plain\" (\"charset\" \"iso-8859-2\") NIL NIL \"quoted-printable\" 185 18 NIL NIL (\"cs\") NIL) 31 NIL (\"attachment\" NIL) NIL NIL) (\"message\" \"rfc822\" NIL NIL NIL \"7bit\" 50592 (\"Sat, 8 Jan 2011 13:58:39 +0100\" \"Subj 1\" ((\"Some Name, SOMECOMPANY\" NIL \"recipient\" \"example.com\")) ((\"Some Name, SOMECOMPANY\" NIL \"recipient\" \"example.com\")) ((\"Some Name, SOMECOMPANY\" NIL \"recipient\" \"example.com\")) ((\"Recipient\" NIL \"example\" \"gmail.com\")) NIL NIL NIL NIL) ( (\"text\" \"plain\" (\"charset\" \"iso-8859-2\") NIL NIL \"quoted-printable\" 4296 345 NIL NIL NIL NIL) (\"text\" \"html\" (\"charset\" \"iso-8859-2\") NIL NIL \"quoted-printable\" 45069 1295 NIL NIL NIL NIL) \"alternative\" (\"boundary\" \"----=_NextPart_000_0073_01CBB179.57530990\") NIL (\"cs\") NIL) 1669 NIL (\"attachment\" NIL) NIL NIL) \"mixed\" (\"boundary\" \"----=_NextPart_000_0077_01CBB179.57530990\") NIL (\"cs\") NIL)\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + BodyPartMultipart multipart; + BodyPart body; + + engine.SetStream (tokenizer); + + try { + body = ImapUtils.ParseBody (engine, "Unexpected token: {0}", string.Empty, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing BODYSTRUCTURE failed: {ex}"); + return; + } + + var token = engine.ReadToken (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (body, Is.InstanceOf (), "Body types did not match."); + multipart = (BodyPartMultipart) body; + + Assert.That (body.ContentType.IsMimeType ("multipart", "mixed"), Is.True, "Content-Type did not match."); + Assert.That (body.ContentType.Parameters["boundary"], Is.EqualTo ("----=_NextPart_000_0077_01CBB179.57530990"), "boundary param did not match"); + Assert.That (multipart.BodyParts, Has.Count.EqualTo (3), "BodyParts count does not match."); + Assert.That (multipart.BodyParts[0], Is.InstanceOf (), "The type of the first child does not match."); + Assert.That (multipart.BodyParts[1], Is.InstanceOf (), "The type of the second child does not match."); + Assert.That (multipart.BodyParts[2], Is.InstanceOf (), "The type of the third child does not match."); + + // FIXME: assert more stuff? + } + } + } + } + + [Test] + public async Task TestParseExampleMultiLevelDovecotBodyStructureAsync () + { + const string text = "(((\"text\" \"plain\" (\"charset\" \"iso-8859-2\") NIL NIL \"quoted-printable\" 28 2 NIL NIL NIL NIL) (\"text\" \"html\" (\"charset\" \"iso-8859-2\") NIL NIL \"quoted-printable\" 1707 65 NIL NIL NIL NIL) \"alternative\" (\"boundary\" \"----=_NextPart_001_0078_01CBB179.57530990\") NIL NIL NIL) (\"message\" \"rfc822\" NIL NIL NIL \"7bit\" 641 (\"Sat, 8 Jan 2011 14:16:36 +0100\" \"Subj 2\" ((\"Some Name, SOMECOMPANY\" NIL \"recipient\" \"example.com\")) ((\"Some Name, SOMECOMPANY\" NIL \"recipient\" \"example.com\")) ((\"Some Name, SOMECOMPANY\" NIL \"recipient\" \"example.com\")) ((\"Recipient\" NIL \"example\" \"gmail.com\")) NIL NIL NIL NIL) (\"text\" \"plain\" (\"charset\" \"iso-8859-2\") NIL NIL \"quoted-printable\" 185 18 NIL NIL (\"cs\") NIL) 31 NIL (\"attachment\" NIL) NIL NIL) (\"message\" \"rfc822\" NIL NIL NIL \"7bit\" 50592 (\"Sat, 8 Jan 2011 13:58:39 +0100\" \"Subj 1\" ((\"Some Name, SOMECOMPANY\" NIL \"recipient\" \"example.com\")) ((\"Some Name, SOMECOMPANY\" NIL \"recipient\" \"example.com\")) ((\"Some Name, SOMECOMPANY\" NIL \"recipient\" \"example.com\")) ((\"Recipient\" NIL \"example\" \"gmail.com\")) NIL NIL NIL NIL) ( (\"text\" \"plain\" (\"charset\" \"iso-8859-2\") NIL NIL \"quoted-printable\" 4296 345 NIL NIL NIL NIL) (\"text\" \"html\" (\"charset\" \"iso-8859-2\") NIL NIL \"quoted-printable\" 45069 1295 NIL NIL NIL NIL) \"alternative\" (\"boundary\" \"----=_NextPart_000_0073_01CBB179.57530990\") NIL (\"cs\") NIL) 1669 NIL (\"attachment\" NIL) NIL NIL) \"mixed\" (\"boundary\" \"----=_NextPart_000_0077_01CBB179.57530990\") NIL (\"cs\") NIL)\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + BodyPartMultipart multipart; + BodyPart body; + + engine.SetStream (tokenizer); + + try { + body = await ImapUtils.ParseBodyAsync (engine, "Unexpected token: {0}", string.Empty, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing BODYSTRUCTURE failed: {ex}"); + return; + } + + var token = await engine.ReadTokenAsync (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (body, Is.InstanceOf (), "Body types did not match."); + multipart = (BodyPartMultipart) body; + + Assert.That (body.ContentType.IsMimeType ("multipart", "mixed"), Is.True, "Content-Type did not match."); + Assert.That (body.ContentType.Parameters["boundary"], Is.EqualTo ("----=_NextPart_000_0077_01CBB179.57530990"), "boundary param did not match"); + Assert.That (multipart.BodyParts, Has.Count.EqualTo (3), "BodyParts count does not match."); + Assert.That (multipart.BodyParts[0], Is.InstanceOf (), "The type of the first child does not match."); + Assert.That (multipart.BodyParts[1], Is.InstanceOf (), "The type of the second child does not match."); + Assert.That (multipart.BodyParts[2], Is.InstanceOf (), "The type of the third child does not match."); + + // FIXME: assert more stuff? + } + } + } + } + + // This tests the work-around for issue #878 + [Test] + public void TestParseBodyStructureWithBrokenMultipartRelated () + { + const string text = "((\"multipart\" \"related\" (\"boundary\" \"----=_@@@@BeautyqueenS87@_@147836_6893840099.85426606923635\") NIL NIL \"7BIT\" 400 (\"boundary\" \"----=_@@@@BeautyqueenS87@_@147836_6893840099.85426606923635\") NIL NIL NIL)(\"TEXT\" \"html\" (\"charset\" \"UTF8\") NIL NIL \"7BIT\" 1115 70 NIL NIL NIL NIL)(\"TEXT\" \"html\" (\"charset\" \"UTF8\") NIL NIL \"QUOTED-PRINTABLE\" 16 2 NIL NIL NIL NIL) \"mixed\" (\"boundary\" \"----=--_DRYORTABLE@@@_@@@8957836_03253840099.78526606923635\") NIL NIL NIL)\r\n"; + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + BodyPart body; + + engine.SetStream (tokenizer); + + try { + body = ImapUtils.ParseBody (engine, "Unexpected token: {0}", string.Empty, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing BODYSTRUCTURE failed: {ex}"); + return; + } + + var token = engine.ReadToken (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (body, Is.InstanceOf (), "Body types did not match."); + var multipart = (BodyPartMultipart) body; + + Assert.That (multipart.ContentType.IsMimeType ("multipart", "mixed"), Is.True, "multipart/mixed Content-Type did not match."); + Assert.That (multipart.ContentType.Parameters["boundary"], Is.EqualTo ("----=--_DRYORTABLE@@@_@@@8957836_03253840099.78526606923635"), "boundary param did not match"); + Assert.That (multipart.BodyParts, Has.Count.EqualTo (3), "BodyParts count did not match."); + + Assert.That (multipart.BodyParts[0], Is.InstanceOf (), "The type of the first child did not match."); + Assert.That (multipart.BodyParts[1], Is.InstanceOf (), "The type of the second child did not match."); + Assert.That (multipart.BodyParts[2], Is.InstanceOf (), "The type of the third child did not match."); + + var related = (BodyPartBasic) multipart.BodyParts[0]; + Assert.That (related.ContentType.IsMimeType ("multipart", "related"), Is.True, "multipart/related Content-Type did not match."); + Assert.That (related.ContentType.Parameters["boundary"], Is.EqualTo ("----=_@@@@BeautyqueenS87@_@147836_6893840099.85426606923635"), "multipart/related boundary param did not match"); + Assert.That (related.ContentTransferEncoding, Is.EqualTo ("7BIT"), "multipart/related Content-Transfer-Encoding did not match."); + Assert.That (related.Octets, Is.EqualTo (400), "multipart/related octets do not match."); + } + } + } + } + + // This tests the work-around for issue #878 + [Test] + public async Task TestParseBodyStructureWithBrokenMultipartRelatedAsync () + { + const string text = "((\"multipart\" \"related\" (\"boundary\" \"----=_@@@@BeautyqueenS87@_@147836_6893840099.85426606923635\") NIL NIL \"7BIT\" 400 (\"boundary\" \"----=_@@@@BeautyqueenS87@_@147836_6893840099.85426606923635\") NIL NIL NIL)(\"TEXT\" \"html\" (\"charset\" \"UTF8\") NIL NIL \"7BIT\" 1115 70 NIL NIL NIL NIL)(\"TEXT\" \"html\" (\"charset\" \"UTF8\") NIL NIL \"QUOTED-PRINTABLE\" 16 2 NIL NIL NIL NIL) \"mixed\" (\"boundary\" \"----=--_DRYORTABLE@@@_@@@8957836_03253840099.78526606923635\") NIL NIL NIL)\r\n"; + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + BodyPart body; + + engine.SetStream (tokenizer); + + try { + body = await ImapUtils.ParseBodyAsync (engine, "Unexpected token: {0}", string.Empty, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing BODYSTRUCTURE failed: {ex}"); + return; + } + + var token = await engine.ReadTokenAsync (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (body, Is.InstanceOf (), "Body types did not match."); + var multipart = (BodyPartMultipart) body; + + Assert.That (multipart.ContentType.IsMimeType ("multipart", "mixed"), Is.True, "multipart/mixed Content-Type did not match."); + Assert.That (multipart.ContentType.Parameters["boundary"], Is.EqualTo ("----=--_DRYORTABLE@@@_@@@8957836_03253840099.78526606923635"), "boundary param did not match"); + Assert.That (multipart.BodyParts, Has.Count.EqualTo (3), "BodyParts count did not match."); + + Assert.That (multipart.BodyParts[0], Is.InstanceOf (), "The type of the first child did not match."); + Assert.That (multipart.BodyParts[1], Is.InstanceOf (), "The type of the second child did not match."); + Assert.That (multipart.BodyParts[2], Is.InstanceOf (), "The type of the third child did not match."); + + var related = (BodyPartBasic) multipart.BodyParts[0]; + Assert.That (related.ContentType.IsMimeType ("multipart", "related"), Is.True, "multipart/related Content-Type did not match."); + Assert.That (related.ContentType.Parameters["boundary"], Is.EqualTo ("----=_@@@@BeautyqueenS87@_@147836_6893840099.85426606923635"), "multipart/related boundary param did not match"); + Assert.That (related.ContentTransferEncoding, Is.EqualTo ("7BIT"), "multipart/related Content-Transfer-Encoding did not match."); + Assert.That (related.Octets, Is.EqualTo (400), "multipart/related octets do not match."); + } + } + } + } + + // This tests the work-around for issue #944 + [Test] + public void TestParseBodyStructureWithEmptyParenListAsMessageRfc822BodyToken () + { + const string text = "((\"text\" \"plain\" (\"charset\" \"UTF-8\") NIL NIL \"base64\" 232 4 NIL NIL NIL)(\"message\" \"delivery-status\" NIL NIL NIL \"7BIT\" 421 NIL NIL NIL)(\"message\" \"rfc822\" NIL NIL NIL \"7BIT\" 787 (NIL NIL NIL NIL NIL NIL NIL NIL NIL NIL) () 0 NIL NIL NIL) \"report\" (\"report-type\" \"delivery-status\" \"boundary\" \"==IFJRGLKFGIR60132UHRUHIHD\") NIL NIL)\r\n"; + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + BodyPart body; + + engine.SetStream (tokenizer); + + try { + body = ImapUtils.ParseBody (engine, "Unexpected token: {0}", string.Empty, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing BODYSTRUCTURE failed: {ex}"); + return; + } + + var token = engine.ReadToken (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (body, Is.InstanceOf (), "Body types did not match."); + var multipart = (BodyPartMultipart) body; + + Assert.That (multipart.ContentType.IsMimeType ("multipart", "report"), Is.True, "multipart/report Content-Type did not match."); + Assert.That (multipart.ContentType.Parameters["boundary"], Is.EqualTo ("==IFJRGLKFGIR60132UHRUHIHD"), "boundary param did not match."); + Assert.That (multipart.ContentType.Parameters["report-type"], Is.EqualTo ("delivery-status"), "report-type param did not match."); + Assert.That (multipart.BodyParts, Has.Count.EqualTo (3), "BodyParts count did not match."); + + Assert.That (multipart.BodyParts[0], Is.InstanceOf (), "The type of the first child did not match."); + Assert.That (multipart.BodyParts[1], Is.InstanceOf (), "The type of the second child did not match."); + Assert.That (multipart.BodyParts[2], Is.InstanceOf (), "The type of the third child did not match."); + + var plain = (BodyPartText) multipart.BodyParts[0]; + Assert.That (plain.ContentType.IsMimeType ("text", "plain"), Is.True, "text/plain Content-Type did not match."); + Assert.That (plain.ContentType.Charset, Is.EqualTo ("UTF-8"), "text/plain charset param did not match."); + Assert.That (plain.ContentTransferEncoding, Is.EqualTo ("base64"), "text/plain encoding did not match."); + Assert.That (plain.Octets, Is.EqualTo (232), "text/plain octets did not match."); + Assert.That (plain.Lines, Is.EqualTo (4), "text/plain lines did not match."); + + var dstat = (BodyPartBasic) multipart.BodyParts[1]; + Assert.That (dstat.ContentType.IsMimeType ("message", "delivery-status"), Is.True, "message/delivery-status Content-Type did not match."); + Assert.That (dstat.ContentTransferEncoding, Is.EqualTo ("7BIT"), "message/delivery-status encoding did not match."); + Assert.That (dstat.Octets, Is.EqualTo (421), "message/delivery-status octets did not match."); + + var rfc822 = (BodyPartMessage) multipart.BodyParts[2]; + Assert.That (rfc822.ContentType.IsMimeType ("message", "rfc822"), Is.True, "message/rfc822 Content-Type did not match."); + Assert.That (rfc822.ContentId, Is.Null, "message/rfc822 Content-Id should be NIL."); + Assert.That (rfc822.ContentDescription, Is.Null, "message/rfc822 Content-Description should be NIL."); + Assert.That (rfc822.Envelope.Sender, Is.Empty, "message/rfc822 Envelope.Sender should be null."); + Assert.That (rfc822.Envelope.From, Is.Empty, "message/rfc822 Envelope.From should be null."); + Assert.That (rfc822.Envelope.ReplyTo, Is.Empty, "message/rfc822 Envelope.ReplyTo should be null."); + Assert.That (rfc822.Envelope.To, Is.Empty, "message/rfc822 Envelope.To should be null."); + Assert.That (rfc822.Envelope.Cc, Is.Empty, "message/rfc822 Envelope.Cc should be null."); + Assert.That (rfc822.Envelope.Bcc, Is.Empty, "message/rfc822 Envelope.Bcc should be null."); + Assert.That (rfc822.Envelope.Subject, Is.Null, "message/rfc822 Envelope.Subject should be null."); + Assert.That (rfc822.Envelope.MessageId, Is.Null, "message/rfc822 Envelope.MessageId should be null."); + Assert.That (rfc822.Envelope.InReplyTo, Is.Null, "message/rfc822 Envelope.InReplyTo should be null."); + Assert.That (rfc822.Envelope.Date, Is.Null, "message/rfc822 Envelope.Date should be null."); + Assert.That (rfc822.ContentTransferEncoding, Is.EqualTo ("7BIT"), "message/rfc822 encoding did not match."); + Assert.That (rfc822.Octets, Is.EqualTo (787), "message/rfc822 octets did not match."); + Assert.That (rfc822.Body, Is.Null, "message/rfc822 body should be null."); + Assert.That (rfc822.Lines, Is.EqualTo (0), "message/rfc822 lines did not match."); + } + } + } + } + + // This tests the work-around for issue #944 + [Test] + public async Task TestParseBodyStructureWithEmptyParenListAsMessageRfc822BodyTokenAsync () + { + const string text = "((\"text\" \"plain\" (\"charset\" \"UTF-8\") NIL NIL \"base64\" 232 4 NIL NIL NIL)(\"message\" \"delivery-status\" NIL NIL NIL \"7BIT\" 421 NIL NIL NIL)(\"message\" \"rfc822\" NIL NIL NIL \"7BIT\" 787 (NIL NIL NIL NIL NIL NIL NIL NIL NIL NIL) () 0 NIL NIL NIL) \"report\" (\"report-type\" \"delivery-status\" \"boundary\" \"==IFJRGLKFGIR60132UHRUHIHD\") NIL NIL)\r\n"; + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + BodyPart body; + + engine.SetStream (tokenizer); + + try { + body = await ImapUtils.ParseBodyAsync (engine, "Unexpected token: {0}", string.Empty, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing BODYSTRUCTURE failed: {ex}"); + return; + } + + var token = await engine.ReadTokenAsync (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (body, Is.InstanceOf (), "Body types did not match."); + var multipart = (BodyPartMultipart) body; + + Assert.That (multipart.ContentType.IsMimeType ("multipart", "report"), Is.True, "multipart/report Content-Type did not match."); + Assert.That (multipart.ContentType.Parameters["boundary"], Is.EqualTo ("==IFJRGLKFGIR60132UHRUHIHD"), "boundary param did not match."); + Assert.That (multipart.ContentType.Parameters["report-type"], Is.EqualTo ("delivery-status"), "report-type param did not match."); + Assert.That (multipart.BodyParts, Has.Count.EqualTo (3), "BodyParts count did not match."); + + Assert.That (multipart.BodyParts[0], Is.InstanceOf (), "The type of the first child did not match."); + Assert.That (multipart.BodyParts[1], Is.InstanceOf (), "The type of the second child did not match."); + Assert.That (multipart.BodyParts[2], Is.InstanceOf (), "The type of the third child did not match."); + + var plain = (BodyPartText) multipart.BodyParts[0]; + Assert.That (plain.ContentType.IsMimeType ("text", "plain"), Is.True, "text/plain Content-Type did not match."); + Assert.That (plain.ContentType.Charset, Is.EqualTo ("UTF-8"), "text/plain charset param did not match."); + Assert.That (plain.ContentTransferEncoding, Is.EqualTo ("base64"), "text/plain encoding did not match."); + Assert.That (plain.Octets, Is.EqualTo (232), "text/plain octets did not match."); + Assert.That (plain.Lines, Is.EqualTo (4), "text/plain lines did not match."); + + var dstat = (BodyPartBasic) multipart.BodyParts[1]; + Assert.That (dstat.ContentType.IsMimeType ("message", "delivery-status"), Is.True, "message/delivery-status Content-Type did not match."); + Assert.That (dstat.ContentTransferEncoding, Is.EqualTo ("7BIT"), "message/delivery-status encoding did not match."); + Assert.That (dstat.Octets, Is.EqualTo (421), "message/delivery-status octets did not match."); + + var rfc822 = (BodyPartMessage) multipart.BodyParts[2]; + Assert.That (rfc822.ContentType.IsMimeType ("message", "rfc822"), Is.True, "message/rfc822 Content-Type did not match."); + Assert.That (rfc822.ContentId, Is.Null, "message/rfc822 Content-Id should be NIL."); + Assert.That (rfc822.ContentDescription, Is.Null, "message/rfc822 Content-Description should be NIL."); + Assert.That (rfc822.Envelope.Sender, Is.Empty, "message/rfc822 Envelope.Sender should be null."); + Assert.That (rfc822.Envelope.From, Is.Empty, "message/rfc822 Envelope.From should be null."); + Assert.That (rfc822.Envelope.ReplyTo, Is.Empty, "message/rfc822 Envelope.ReplyTo should be null."); + Assert.That (rfc822.Envelope.To, Is.Empty, "message/rfc822 Envelope.To should be null."); + Assert.That (rfc822.Envelope.Cc, Is.Empty, "message/rfc822 Envelope.Cc should be null."); + Assert.That (rfc822.Envelope.Bcc, Is.Empty, "message/rfc822 Envelope.Bcc should be null."); + Assert.That (rfc822.Envelope.Subject, Is.Null, "message/rfc822 Envelope.Subject should be null."); + Assert.That (rfc822.Envelope.MessageId, Is.Null, "message/rfc822 Envelope.MessageId should be null."); + Assert.That (rfc822.Envelope.InReplyTo, Is.Null, "message/rfc822 Envelope.InReplyTo should be null."); + Assert.That (rfc822.Envelope.Date, Is.Null, "message/rfc822 Envelope.Date should be null."); + Assert.That (rfc822.ContentTransferEncoding, Is.EqualTo ("7BIT"), "message/rfc822 encoding did not match."); + Assert.That (rfc822.Octets, Is.EqualTo (787), "message/rfc822 octets did not match."); + Assert.That (rfc822.Body, Is.Null, "message/rfc822 body should be null."); + Assert.That (rfc822.Lines, Is.EqualTo (0), "message/rfc822 lines did not match."); + } + } + } + } + + [Test] + public void TestParseBodyStructureWithContentMd5DspLanguageAndLocation () + { + const string text = "((\"text\" \"plain\" (\"charset\" \"iso-8859-1\") NIL NIL \"quoted-printable\" 28 2 \"md5sum\" (\"inline\" (\"filename\" \"body.txt\")) \"en\" \"http://www.google.com/body.txt\") (\"text\" \"html\" (\"charset\" \"iso-8859-1\") NIL NIL \"quoted-printable\" 1707 65 \"md5sum\" (\"inline\" (\"filename\" \"body.html\")) \"en\" \"http://www.google.com/body.html\") \"alternative\" (\"boundary\" \"----=_NextPart_001_0078_01CBB179.57530990\") (\"inline\" (\"filename\" \"alternative.txt\")) \"en\" \"http://www.google.com/alternative.txt\")\r\n"; + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + BodyPart body; + + engine.SetStream (tokenizer); + + try { + body = ImapUtils.ParseBody (engine, "Unexpected token: {0}", string.Empty, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing BODYSTRUCTURE failed: {ex}"); + return; + } + + var token = engine.ReadToken (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (body, Is.InstanceOf (), "Body types did not match."); + var multipart = (BodyPartMultipart) body; + + Assert.That (multipart.ContentType.IsMimeType ("multipart", "alternative"), Is.True, "multipart/alternative Content-Type did not match."); + Assert.That (multipart.ContentType.Parameters["boundary"], Is.EqualTo ("----=_NextPart_001_0078_01CBB179.57530990"), "boundary param did not match"); + Assert.That (multipart.ContentDisposition.Disposition, Is.EqualTo ("inline"), "multipart/alternative disposition did not match"); + Assert.That (multipart.ContentDisposition.FileName, Is.EqualTo ("alternative.txt"), "multipart/alternative filename did not match"); + Assert.That (multipart.ContentLanguage, Is.Not.Null, "multipart/alternative Content-Language should not be null"); + Assert.That (multipart.ContentLanguage, Has.Length.EqualTo (1), "multipart/alternative Content-Language count did not match"); + Assert.That (multipart.ContentLanguage[0], Is.EqualTo ("en"), "multipart/alternative Content-Language value did not match"); + Assert.That (multipart.ContentLocation.ToString (), Is.EqualTo ("http://www.google.com/alternative.txt"), "multipart/alternative location did not match"); + Assert.That (multipart.BodyParts, Has.Count.EqualTo (2), "BodyParts count did not match."); + + Assert.That (multipart.BodyParts[0], Is.InstanceOf (), "The type of the first child did not match."); + Assert.That (multipart.BodyParts[1], Is.InstanceOf (), "The type of the second child did not match."); + + var plain = (BodyPartText) multipart.BodyParts[0]; + Assert.That (plain.ContentType.IsMimeType ("text", "plain"), Is.True, "text/plain Content-Type did not match."); + Assert.That (plain.ContentType.Charset, Is.EqualTo ("iso-8859-1"), "text/plain charset param did not match"); + Assert.That (plain.ContentDisposition.Disposition, Is.EqualTo ("inline"), "text/plain disposition did not match"); + Assert.That (plain.ContentDisposition.FileName, Is.EqualTo ("body.txt"), "text/plain filename did not match"); + Assert.That (plain.ContentMd5, Is.EqualTo ("md5sum"), "text/html Content-Md5 did not match"); + Assert.That (plain.ContentLanguage, Is.Not.Null, "text/plain Content-Language should not be null"); + Assert.That (plain.ContentLanguage, Has.Length.EqualTo (1), "text/plain Content-Language count did not match"); + Assert.That (plain.ContentLanguage [0], Is.EqualTo ("en"), "text/plain Content-Language value did not match"); + Assert.That (plain.ContentLocation.ToString (), Is.EqualTo ("http://www.google.com/body.txt"), "text/plain location did not match"); + Assert.That (plain.Octets, Is.EqualTo (28), "text/plain octets did not match"); + Assert.That (plain.Lines, Is.EqualTo (2), "text/plain lines did not match"); + + var html = (BodyPartText) multipart.BodyParts[1]; + Assert.That (html.ContentType.IsMimeType ("text", "html"), Is.True, "text/html Content-Type did not match."); + Assert.That (html.ContentType.Charset, Is.EqualTo ("iso-8859-1"), "text/html charset param did not match"); + Assert.That (html.ContentDisposition.Disposition, Is.EqualTo ("inline"), "text/html disposition did not match"); + Assert.That (html.ContentDisposition.FileName, Is.EqualTo ("body.html"), "text/html filename did not match"); + Assert.That (html.ContentMd5, Is.EqualTo ("md5sum"), "text/html Content-Md5 did not match"); + Assert.That (html.ContentLanguage, Is.Not.Null, "text/html Content-Language should not be null"); + Assert.That (html.ContentLanguage, Has.Length.EqualTo (1), "text/html Content-Language count did not match"); + Assert.That (html.ContentLanguage [0], Is.EqualTo ("en"), "text/html Content-Language value did not match"); + Assert.That (html.ContentLocation.ToString (), Is.EqualTo ("http://www.google.com/body.html"), "text/html location did not match"); + Assert.That (html.Octets, Is.EqualTo (1707), "text/html octets did not match"); + Assert.That (html.Lines, Is.EqualTo (65), "text/html lines did not match"); + } + } + } + } + + [Test] + public async Task TestParseBodyStructureWithContentMd5DspLanguageAndLocationAsync () + { + const string text = "((\"text\" \"plain\" (\"charset\" \"iso-8859-1\") NIL NIL \"quoted-printable\" 28 2 \"md5sum\" (\"inline\" (\"filename\" \"body.txt\")) \"en\" \"http://www.google.com/body.txt\") (\"text\" \"html\" (\"charset\" \"iso-8859-1\") NIL NIL \"quoted-printable\" 1707 65 \"md5sum\" (\"inline\" (\"filename\" \"body.html\")) \"en\" \"http://www.google.com/body.html\") \"alternative\" (\"boundary\" \"----=_NextPart_001_0078_01CBB179.57530990\") (\"inline\" (\"filename\" \"alternative.txt\")) \"en\" \"http://www.google.com/alternative.txt\")\r\n"; + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + BodyPart body; + + engine.SetStream (tokenizer); + + try { + body = await ImapUtils.ParseBodyAsync (engine, "Unexpected token: {0}", string.Empty, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing BODYSTRUCTURE failed: {ex}"); + return; + } + + var token = await engine.ReadTokenAsync (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (body, Is.InstanceOf (), "Body types did not match."); + var multipart = (BodyPartMultipart) body; + + Assert.That (multipart.ContentType.IsMimeType ("multipart", "alternative"), Is.True, "multipart/alternative Content-Type did not match."); + Assert.That (multipart.ContentType.Parameters["boundary"], Is.EqualTo ("----=_NextPart_001_0078_01CBB179.57530990"), "boundary param did not match"); + Assert.That (multipart.ContentDisposition.Disposition, Is.EqualTo ("inline"), "multipart/alternative disposition did not match"); + Assert.That (multipart.ContentDisposition.FileName, Is.EqualTo ("alternative.txt"), "multipart/alternative filename did not match"); + Assert.That (multipart.ContentLanguage, Is.Not.Null, "multipart/alternative Content-Language should not be null"); + Assert.That (multipart.ContentLanguage, Has.Length.EqualTo (1), "multipart/alternative Content-Language count did not match"); + Assert.That (multipart.ContentLanguage[0], Is.EqualTo ("en"), "multipart/alternative Content-Language value did not match"); + Assert.That (multipart.ContentLocation.ToString (), Is.EqualTo ("http://www.google.com/alternative.txt"), "multipart/alternative location did not match"); + Assert.That (multipart.BodyParts, Has.Count.EqualTo (2), "BodyParts count did not match."); + + Assert.That (multipart.BodyParts[0], Is.InstanceOf (), "The type of the first child did not match."); + Assert.That (multipart.BodyParts[1], Is.InstanceOf (), "The type of the second child did not match."); + + var plain = (BodyPartText) multipart.BodyParts[0]; + Assert.That (plain.ContentType.IsMimeType ("text", "plain"), Is.True, "text/plain Content-Type did not match."); + Assert.That (plain.ContentType.Charset, Is.EqualTo ("iso-8859-1"), "text/plain charset param did not match"); + Assert.That (plain.ContentDisposition.Disposition, Is.EqualTo ("inline"), "text/plain disposition did not match"); + Assert.That (plain.ContentDisposition.FileName, Is.EqualTo ("body.txt"), "text/plain filename did not match"); + Assert.That (plain.ContentMd5, Is.EqualTo ("md5sum"), "text/html Content-Md5 did not match"); + Assert.That (plain.ContentLanguage, Is.Not.Null, "text/plain Content-Language should not be null"); + Assert.That (plain.ContentLanguage, Has.Length.EqualTo (1), "text/plain Content-Language count did not match"); + Assert.That (plain.ContentLanguage[0], Is.EqualTo ("en"), "text/plain Content-Language value did not match"); + Assert.That (plain.ContentLocation.ToString (), Is.EqualTo ("http://www.google.com/body.txt"), "text/plain location did not match"); + Assert.That (plain.Octets, Is.EqualTo (28), "text/plain octets did not match"); + Assert.That (plain.Lines, Is.EqualTo (2), "text/plain lines did not match"); + + var html = (BodyPartText) multipart.BodyParts[1]; + Assert.That (html.ContentType.IsMimeType ("text", "html"), Is.True, "text/html Content-Type did not match."); + Assert.That (html.ContentType.Charset, Is.EqualTo ("iso-8859-1"), "text/html charset param did not match"); + Assert.That (html.ContentDisposition.Disposition, Is.EqualTo ("inline"), "text/html disposition did not match"); + Assert.That (html.ContentDisposition.FileName, Is.EqualTo ("body.html"), "text/html filename did not match"); + Assert.That (html.ContentMd5, Is.EqualTo ("md5sum"), "text/html Content-Md5 did not match"); + Assert.That (html.ContentLanguage, Is.Not.Null, "text/html Content-Language should not be null"); + Assert.That (html.ContentLanguage, Has.Length.EqualTo (1), "text/html Content-Language count did not match"); + Assert.That (html.ContentLanguage[0], Is.EqualTo ("en"), "text/html Content-Language value did not match"); + Assert.That (html.ContentLocation.ToString (), Is.EqualTo ("http://www.google.com/body.html"), "text/html location did not match"); + Assert.That (html.Octets, Is.EqualTo (1707), "text/html octets did not match"); + Assert.That (html.Lines, Is.EqualTo (65), "text/html lines did not match"); + } + } + } + } + + [Test] + public void TestParseBodyStructureWithLiterals_LiteralsEverywhere () + { + const string text = "(({4}\r\ntext {5}\r\nplain ({7}\r\ncharset {10}\r\niso-8859-1) NIL NIL {16}\r\nquoted-printable 28 2 {6}\r\nmd5sum ({6}\r\ninline ({8}\r\nfilename {8}\r\nbody.txt)) {2}\r\nen {30}\r\nhttp://www.google.com/body.txt) (\"text\" \"html\" (\"charset\" \"iso-8859-1\") NIL NIL \"quoted-printable\" 1707 65 \"md5sum\" (\"inline\" (\"filename\" \"body.html\")) \"en\" \"http://www.google.com/body.html\") {11}\r\nalternative (\"boundary\" \"----=_NextPart_001_0078_01CBB179.57530990\") (\"inline\" (\"filename\" \"alternative.txt\")) \"en\" \"http://www.google.com/alternative.txt\")\r\n"; + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + BodyPart body; + + engine.SetStream (tokenizer); + + try { + body = ImapUtils.ParseBody (engine, "Unexpected token: {0}", string.Empty, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing BODYSTRUCTURE failed: {ex}"); + return; + } + + var token = engine.ReadToken (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (body, Is.InstanceOf (), "Body types did not match."); + var multipart = (BodyPartMultipart)body; + + Assert.That (multipart.ContentType.IsMimeType ("multipart", "alternative"), Is.True, "multipart/alternative Content-Type did not match."); + Assert.That (multipart.ContentType.Parameters ["boundary"], Is.EqualTo ("----=_NextPart_001_0078_01CBB179.57530990"), "boundary param did not match"); + Assert.That (multipart.ContentDisposition.Disposition, Is.EqualTo ("inline"), "multipart/alternative disposition did not match"); + Assert.That (multipart.ContentDisposition.FileName, Is.EqualTo ("alternative.txt"), "multipart/alternative filename did not match"); + Assert.That (multipart.ContentLanguage, Is.Not.Null, "multipart/alternative Content-Language should not be null"); + Assert.That (multipart.ContentLanguage, Has.Length.EqualTo (1), "multipart/alternative Content-Language count did not match"); + Assert.That (multipart.ContentLanguage [0], Is.EqualTo ("en"), "multipart/alternative Content-Language value did not match"); + Assert.That (multipart.ContentLocation.ToString (), Is.EqualTo ("http://www.google.com/alternative.txt"), "multipart/alternative location did not match"); + Assert.That (multipart.BodyParts, Has.Count.EqualTo (2), "BodyParts count did not match."); + + Assert.That (multipart.BodyParts [0], Is.InstanceOf (), "The type of the first child did not match."); + Assert.That (multipart.BodyParts [1], Is.InstanceOf (), "The type of the second child did not match."); + + var plain = (BodyPartText)multipart.BodyParts [0]; + Assert.That (plain.ContentType.IsMimeType ("text", "plain"), Is.True, "text/plain Content-Type did not match."); + Assert.That (plain.ContentType.Charset, Is.EqualTo ("iso-8859-1"), "text/plain charset param did not match"); + Assert.That (plain.ContentDisposition.Disposition, Is.EqualTo ("inline"), "text/plain disposition did not match"); + Assert.That (plain.ContentDisposition.FileName, Is.EqualTo ("body.txt"), "text/plain filename did not match"); + Assert.That (plain.ContentMd5, Is.EqualTo ("md5sum"), "text/html Content-Md5 did not match"); + Assert.That (plain.ContentLanguage, Is.Not.Null, "text/plain Content-Language should not be null"); + Assert.That (plain.ContentLanguage, Has.Length.EqualTo (1), "text/plain Content-Language count did not match"); + Assert.That (plain.ContentLanguage [0], Is.EqualTo ("en"), "text/plain Content-Language value did not match"); + Assert.That (plain.ContentLocation.ToString (), Is.EqualTo ("http://www.google.com/body.txt"), "text/plain location did not match"); + Assert.That (plain.Octets, Is.EqualTo (28), "text/plain octets did not match"); + Assert.That (plain.Lines, Is.EqualTo (2), "text/plain lines did not match"); + + var html = (BodyPartText)multipart.BodyParts [1]; + Assert.That (html.ContentType.IsMimeType ("text", "html"), Is.True, "text/html Content-Type did not match."); + Assert.That (html.ContentType.Charset, Is.EqualTo ("iso-8859-1"), "text/html charset param did not match"); + Assert.That (html.ContentDisposition.Disposition, Is.EqualTo ("inline"), "text/html disposition did not match"); + Assert.That (html.ContentDisposition.FileName, Is.EqualTo ("body.html"), "text/html filename did not match"); + Assert.That (html.ContentMd5, Is.EqualTo ("md5sum"), "text/html Content-Md5 did not match"); + Assert.That (html.ContentLanguage, Is.Not.Null, "text/html Content-Language should not be null"); + Assert.That (html.ContentLanguage, Has.Length.EqualTo (1), "text/html Content-Language count did not match"); + Assert.That (html.ContentLanguage [0], Is.EqualTo ("en"), "text/html Content-Language value did not match"); + Assert.That (html.ContentLocation.ToString (), Is.EqualTo ("http://www.google.com/body.html"), "text/html location did not match"); + Assert.That (html.Octets, Is.EqualTo (1707), "text/html octets did not match"); + Assert.That (html.Lines, Is.EqualTo (65), "text/html lines did not match"); + } + } + } + } + + [Test] + public async Task TestParseBodyStructureWithLiterals_LiteralsEverywhereAsync () + { + const string text = "(({4}\r\ntext {5}\r\nplain ({7}\r\ncharset {10}\r\niso-8859-1) NIL NIL {16}\r\nquoted-printable 28 2 {6}\r\nmd5sum ({6}\r\ninline ({8}\r\nfilename {8}\r\nbody.txt)) {2}\r\nen {30}\r\nhttp://www.google.com/body.txt) (\"text\" \"html\" (\"charset\" \"iso-8859-1\") NIL NIL \"quoted-printable\" 1707 65 \"md5sum\" (\"inline\" (\"filename\" \"body.html\")) \"en\" \"http://www.google.com/body.html\") {11}\r\nalternative (\"boundary\" \"----=_NextPart_001_0078_01CBB179.57530990\") (\"inline\" (\"filename\" \"alternative.txt\")) \"en\" \"http://www.google.com/alternative.txt\")\r\n"; + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + BodyPart body; + + engine.SetStream (tokenizer); + + try { + body = await ImapUtils.ParseBodyAsync (engine, "Unexpected token: {0}", string.Empty, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing BODYSTRUCTURE failed: {ex}"); + return; + } + + var token = await engine.ReadTokenAsync (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (body, Is.InstanceOf (), "Body types did not match."); + var multipart = (BodyPartMultipart) body; + + Assert.That (multipart.ContentType.IsMimeType ("multipart", "alternative"), Is.True, "multipart/alternative Content-Type did not match."); + Assert.That (multipart.ContentType.Parameters["boundary"], Is.EqualTo ("----=_NextPart_001_0078_01CBB179.57530990"), "boundary param did not match"); + Assert.That (multipart.ContentDisposition.Disposition, Is.EqualTo ("inline"), "multipart/alternative disposition did not match"); + Assert.That (multipart.ContentDisposition.FileName, Is.EqualTo ("alternative.txt"), "multipart/alternative filename did not match"); + Assert.That (multipart.ContentLanguage, Is.Not.Null, "multipart/alternative Content-Language should not be null"); + Assert.That (multipart.ContentLanguage, Has.Length.EqualTo (1), "multipart/alternative Content-Language count did not match"); + Assert.That (multipart.ContentLanguage[0], Is.EqualTo ("en"), "multipart/alternative Content-Language value did not match"); + Assert.That (multipart.ContentLocation.ToString (), Is.EqualTo ("http://www.google.com/alternative.txt"), "multipart/alternative location did not match"); + Assert.That (multipart.BodyParts, Has.Count.EqualTo (2), "BodyParts count did not match."); + + Assert.That (multipart.BodyParts[0], Is.InstanceOf (), "The type of the first child did not match."); + Assert.That (multipart.BodyParts[1], Is.InstanceOf (), "The type of the second child did not match."); + + var plain = (BodyPartText) multipart.BodyParts[0]; + Assert.That (plain.ContentType.IsMimeType ("text", "plain"), Is.True, "text/plain Content-Type did not match."); + Assert.That (plain.ContentType.Charset, Is.EqualTo ("iso-8859-1"), "text/plain charset param did not match"); + Assert.That (plain.ContentDisposition.Disposition, Is.EqualTo ("inline"), "text/plain disposition did not match"); + Assert.That (plain.ContentDisposition.FileName, Is.EqualTo ("body.txt"), "text/plain filename did not match"); + Assert.That (plain.ContentMd5, Is.EqualTo ("md5sum"), "text/html Content-Md5 did not match"); + Assert.That (plain.ContentLanguage, Is.Not.Null, "text/plain Content-Language should not be null"); + Assert.That (plain.ContentLanguage, Has.Length.EqualTo (1), "text/plain Content-Language count did not match"); + Assert.That (plain.ContentLanguage[0], Is.EqualTo ("en"), "text/plain Content-Language value did not match"); + Assert.That (plain.ContentLocation.ToString (), Is.EqualTo ("http://www.google.com/body.txt"), "text/plain location did not match"); + Assert.That (plain.Octets, Is.EqualTo (28), "text/plain octets did not match"); + Assert.That (plain.Lines, Is.EqualTo (2), "text/plain lines did not match"); + + var html = (BodyPartText) multipart.BodyParts[1]; + Assert.That (html.ContentType.IsMimeType ("text", "html"), Is.True, "text/html Content-Type did not match."); + Assert.That (html.ContentType.Charset, Is.EqualTo ("iso-8859-1"), "text/html charset param did not match"); + Assert.That (html.ContentDisposition.Disposition, Is.EqualTo ("inline"), "text/html disposition did not match"); + Assert.That (html.ContentDisposition.FileName, Is.EqualTo ("body.html"), "text/html filename did not match"); + Assert.That (html.ContentMd5, Is.EqualTo ("md5sum"), "text/html Content-Md5 did not match"); + Assert.That (html.ContentLanguage, Is.Not.Null, "text/html Content-Language should not be null"); + Assert.That (html.ContentLanguage, Has.Length.EqualTo (1), "text/html Content-Language count did not match"); + Assert.That (html.ContentLanguage[0], Is.EqualTo ("en"), "text/html Content-Language value did not match"); + Assert.That (html.ContentLocation.ToString (), Is.EqualTo ("http://www.google.com/body.html"), "text/html location did not match"); + Assert.That (html.Octets, Is.EqualTo (1707), "text/html octets did not match"); + Assert.That (html.Lines, Is.EqualTo (65), "text/html lines did not match"); + } + } + } + } + + [Test] + public void TestParseBodyStructureWithBodyExtensions () + { + const string text = "((\"text\" \"plain\" (\"charset\" \"iso-8859-1\") NIL NIL \"quoted-printable\" 28 2 \"md5sum\" (\"inline\" (\"filename\" \"body.txt\")) \"en\" \"body.txt\" \"extension1\" 123 (\"extension2\" (\"nested-extension3\" \"nested-extension4\"))) (\"text\" \"html\" (\"charset\" \"iso-8859-1\") NIL NIL \"quoted-printable\" 1707 65 \"md5sum\" (\"inline\" (\"filename\" \"body.html\")) \"en\" \"http://www.google.com/body.html\") \"alternative\" (\"boundary\" \"----=_NextPart_001_0078_01CBB179.57530990\") (\"inline\" (\"filename\" \"alternative.txt\")) \"en\" \"http://www.google.com/alternative.txt\" {10}\r\nextension1 123 (\"extension2\" (\"nested-extension3\" \"nested-extension4\")))\r\n"; + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + BodyPart body; + + engine.SetStream (tokenizer); + + try { + body = ImapUtils.ParseBody (engine, "Unexpected token: {0}", string.Empty, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing BODYSTRUCTURE failed: {ex}"); + return; + } + + var token = engine.ReadToken (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (body, Is.InstanceOf (), "Body types did not match."); + var multipart = (BodyPartMultipart)body; + + Assert.That (multipart.ContentType.IsMimeType ("multipart", "alternative"), Is.True, "multipart/alternative Content-Type did not match."); + Assert.That (multipart.ContentType.Parameters ["boundary"], Is.EqualTo ("----=_NextPart_001_0078_01CBB179.57530990"), "boundary param did not match"); + Assert.That (multipart.ContentDisposition.Disposition, Is.EqualTo ("inline"), "multipart/alternative disposition did not match"); + Assert.That (multipart.ContentDisposition.FileName, Is.EqualTo ("alternative.txt"), "multipart/alternative filename did not match"); + Assert.That (multipart.ContentLanguage, Is.Not.Null, "multipart/alternative Content-Language should not be null"); + Assert.That (multipart.ContentLanguage, Has.Length.EqualTo (1), "multipart/alternative Content-Language count did not match"); + Assert.That (multipart.ContentLanguage [0], Is.EqualTo ("en"), "multipart/alternative Content-Language value did not match"); + Assert.That (multipart.ContentLocation.ToString (), Is.EqualTo ("http://www.google.com/alternative.txt"), "multipart/alternative location did not match"); + Assert.That (multipart.BodyParts, Has.Count.EqualTo (2), "BodyParts count did not match."); + + Assert.That (multipart.BodyParts [0], Is.InstanceOf (), "The type of the first child did not match."); + Assert.That (multipart.BodyParts [1], Is.InstanceOf (), "The type of the second child did not match."); + + var plain = (BodyPartText)multipart.BodyParts [0]; + Assert.That (plain.ContentType.IsMimeType ("text", "plain"), Is.True, "text/plain Content-Type did not match."); + Assert.That (plain.ContentType.Charset, Is.EqualTo ("iso-8859-1"), "text/plain charset param did not match"); + Assert.That (plain.ContentDisposition.Disposition, Is.EqualTo ("inline"), "text/plain disposition did not match"); + Assert.That (plain.ContentDisposition.FileName, Is.EqualTo ("body.txt"), "text/plain filename did not match"); + Assert.That (plain.ContentMd5, Is.EqualTo ("md5sum"), "text/html Content-Md5 did not match"); + Assert.That (plain.ContentLanguage, Is.Not.Null, "text/plain Content-Language should not be null"); + Assert.That (plain.ContentLanguage, Has.Length.EqualTo (1), "text/plain Content-Language count did not match"); + Assert.That (plain.ContentLanguage [0], Is.EqualTo ("en"), "text/plain Content-Language value did not match"); + Assert.That (plain.ContentLocation.ToString (), Is.EqualTo ("body.txt"), "text/plain location did not match"); + Assert.That (plain.Octets, Is.EqualTo (28), "text/plain octets did not match"); + Assert.That (plain.Lines, Is.EqualTo (2), "text/plain lines did not match"); + + var html = (BodyPartText)multipart.BodyParts [1]; + Assert.That (html.ContentType.IsMimeType ("text", "html"), Is.True, "text/html Content-Type did not match."); + Assert.That (html.ContentType.Charset, Is.EqualTo ("iso-8859-1"), "text/html charset param did not match"); + Assert.That (html.ContentDisposition.Disposition, Is.EqualTo ("inline"), "text/html disposition did not match"); + Assert.That (html.ContentDisposition.FileName, Is.EqualTo ("body.html"), "text/html filename did not match"); + Assert.That (html.ContentMd5, Is.EqualTo ("md5sum"), "text/html Content-Md5 did not match"); + Assert.That (html.ContentLanguage, Is.Not.Null, "text/html Content-Language should not be null"); + Assert.That (html.ContentLanguage, Has.Length.EqualTo (1), "text/html Content-Language count did not match"); + Assert.That (html.ContentLanguage [0], Is.EqualTo ("en"), "text/html Content-Language value did not match"); + Assert.That (html.ContentLocation.ToString (), Is.EqualTo ("http://www.google.com/body.html"), "text/html location did not match"); + Assert.That (html.Octets, Is.EqualTo (1707), "text/html octets did not match"); + Assert.That (html.Lines, Is.EqualTo (65), "text/html lines did not match"); + } + } + } + } + + [Test] + public async Task TestParseBodyStructureWithBodyExtensionsAsync () + { + const string text = "((\"text\" \"plain\" (\"charset\" \"iso-8859-1\") NIL NIL \"quoted-printable\" 28 2 \"md5sum\" (\"inline\" (\"filename\" \"body.txt\")) \"en\" \"body.txt\" \"extension1\" 123 (\"extension2\" (\"nested-extension3\" \"nested-extension4\"))) (\"text\" \"html\" (\"charset\" \"iso-8859-1\") NIL NIL \"quoted-printable\" 1707 65 \"md5sum\" (\"inline\" (\"filename\" \"body.html\")) \"en\" \"http://www.google.com/body.html\") \"alternative\" (\"boundary\" \"----=_NextPart_001_0078_01CBB179.57530990\") (\"inline\" (\"filename\" \"alternative.txt\")) \"en\" \"http://www.google.com/alternative.txt\" {10}\r\nextension1 123 (\"extension2\" (\"nested-extension3\" \"nested-extension4\")))\r\n"; + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + BodyPart body; + + engine.SetStream (tokenizer); + + try { + body = await ImapUtils.ParseBodyAsync (engine, "Unexpected token: {0}", string.Empty, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing BODYSTRUCTURE failed: {ex}"); + return; + } + + var token = await engine.ReadTokenAsync (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (body, Is.InstanceOf (), "Body types did not match."); + var multipart = (BodyPartMultipart) body; + + Assert.That (multipart.ContentType.IsMimeType ("multipart", "alternative"), Is.True, "multipart/alternative Content-Type did not match."); + Assert.That (multipart.ContentType.Parameters["boundary"], Is.EqualTo ("----=_NextPart_001_0078_01CBB179.57530990"), "boundary param did not match"); + Assert.That (multipart.ContentDisposition.Disposition, Is.EqualTo ("inline"), "multipart/alternative disposition did not match"); + Assert.That (multipart.ContentDisposition.FileName, Is.EqualTo ("alternative.txt"), "multipart/alternative filename did not match"); + Assert.That (multipart.ContentLanguage, Is.Not.Null, "multipart/alternative Content-Language should not be null"); + Assert.That (multipart.ContentLanguage, Has.Length.EqualTo (1), "multipart/alternative Content-Language count did not match"); + Assert.That (multipart.ContentLanguage[0], Is.EqualTo ("en"), "multipart/alternative Content-Language value did not match"); + Assert.That (multipart.ContentLocation.ToString (), Is.EqualTo ("http://www.google.com/alternative.txt"), "multipart/alternative location did not match"); + Assert.That (multipart.BodyParts, Has.Count.EqualTo (2), "BodyParts count did not match."); + + Assert.That (multipart.BodyParts[0], Is.InstanceOf (), "The type of the first child did not match."); + Assert.That (multipart.BodyParts[1], Is.InstanceOf (), "The type of the second child did not match."); + + var plain = (BodyPartText) multipart.BodyParts[0]; + Assert.That (plain.ContentType.IsMimeType ("text", "plain"), Is.True, "text/plain Content-Type did not match."); + Assert.That (plain.ContentType.Charset, Is.EqualTo ("iso-8859-1"), "text/plain charset param did not match"); + Assert.That (plain.ContentDisposition.Disposition, Is.EqualTo ("inline"), "text/plain disposition did not match"); + Assert.That (plain.ContentDisposition.FileName, Is.EqualTo ("body.txt"), "text/plain filename did not match"); + Assert.That (plain.ContentMd5, Is.EqualTo ("md5sum"), "text/html Content-Md5 did not match"); + Assert.That (plain.ContentLanguage, Is.Not.Null, "text/plain Content-Language should not be null"); + Assert.That (plain.ContentLanguage, Has.Length.EqualTo (1), "text/plain Content-Language count did not match"); + Assert.That (plain.ContentLanguage[0], Is.EqualTo ("en"), "text/plain Content-Language value did not match"); + Assert.That (plain.ContentLocation.ToString (), Is.EqualTo ("body.txt"), "text/plain location did not match"); + Assert.That (plain.Octets, Is.EqualTo (28), "text/plain octets did not match"); + Assert.That (plain.Lines, Is.EqualTo (2), "text/plain lines did not match"); + + var html = (BodyPartText) multipart.BodyParts[1]; + Assert.That (html.ContentType.IsMimeType ("text", "html"), Is.True, "text/html Content-Type did not match."); + Assert.That (html.ContentType.Charset, Is.EqualTo ("iso-8859-1"), "text/html charset param did not match"); + Assert.That (html.ContentDisposition.Disposition, Is.EqualTo ("inline"), "text/html disposition did not match"); + Assert.That (html.ContentDisposition.FileName, Is.EqualTo ("body.html"), "text/html filename did not match"); + Assert.That (html.ContentMd5, Is.EqualTo ("md5sum"), "text/html Content-Md5 did not match"); + Assert.That (html.ContentLanguage, Is.Not.Null, "text/html Content-Language should not be null"); + Assert.That (html.ContentLanguage, Has.Length.EqualTo (1), "text/html Content-Language count did not match"); + Assert.That (html.ContentLanguage[0], Is.EqualTo ("en"), "text/html Content-Language value did not match"); + Assert.That (html.ContentLocation.ToString (), Is.EqualTo ("http://www.google.com/body.html"), "text/html location did not match"); + Assert.That (html.Octets, Is.EqualTo (1707), "text/html octets did not match"); + Assert.That (html.Lines, Is.EqualTo (65), "text/html lines did not match"); + } + } + } + } + + // This tests the work-around for issue #205 + [Test] + public void TestParseGMailBadlyFormedMultipartBodyStructure () + { + const string text = "((\"ALTERNATIVE\" (\"BOUNDARY\" \"==alternative_xad5934455aeex\") NIL NIL)(\"TEXT\" \"HTML\" (\"CHARSET\" \"iso-8859-1\" \"NAME\" \"seti_letter.html\") NIL NIL \"7BIT\" 6769 171 NIL NIL NIL) \"ALTERNATIVE\" (\"BOUNDARY\" \"==alternative_xad5934455aeex\") NIL NIL)\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + BodyPartMultipart multipart, broken; + BodyPartText html; + BodyPart body; + + engine.SetStream (tokenizer); + engine.QuirksMode = ImapQuirksMode.GMail; + + try { + body = ImapUtils.ParseBody (engine, "Unexpected token: {0}", string.Empty, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing BODYSTRUCTURE failed: {ex}"); + return; + } + + var token = engine.ReadToken (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (body, Is.InstanceOf (), "Body types did not match."); + multipart = (BodyPartMultipart) body; + + Assert.That (multipart.ContentType.IsMimeType ("multipart", "alternative"), Is.True, "outer multipart/alternative Content-Type did not match."); + Assert.That (multipart.ContentType.Parameters["boundary"], Is.EqualTo ("==alternative_xad5934455aeex"), "outer multipart/alternative boundary param did not match"); + Assert.That (multipart.BodyParts, Has.Count.EqualTo (2), "outer multipart/alternative BodyParts count does not match."); + + Assert.That (multipart.BodyParts[0], Is.InstanceOf (), "The type of the first child does not match."); + broken = (BodyPartMultipart) multipart.BodyParts[0]; + Assert.That (broken.ContentType.IsMimeType ("multipart", "alternative"), Is.True, "inner multipart/alternative Content-Type did not match."); + Assert.That (broken.ContentType.Parameters["boundary"], Is.EqualTo ("==alternative_xad5934455aeex"), "inner multipart/alternative boundary param did not match"); + Assert.That (broken.BodyParts, Is.Empty, "inner multipart/alternative BodyParts count does not match."); + + Assert.That (multipart.BodyParts[1], Is.InstanceOf (), "The type of the second child does not match."); + html = (BodyPartText) multipart.BodyParts[1]; + Assert.That (html.ContentType.IsMimeType ("text", "html"), Is.True, "text/html Content-Type did not match."); + Assert.That (html.ContentType.Charset, Is.EqualTo ("iso-8859-1"), "text/html charset parameter did not match"); + Assert.That (html.ContentType.Name, Is.EqualTo ("seti_letter.html"), "text/html name parameter did not match"); + } + } + } + } + + // This tests the work-around for issue #205 + [Test] + public async Task TestParseGMailBadlyFormedMultipartBodyStructureAsync () + { + const string text = "((\"ALTERNATIVE\" (\"BOUNDARY\" \"==alternative_xad5934455aeex\") NIL NIL)(\"TEXT\" \"HTML\" (\"CHARSET\" \"iso-8859-1\" \"NAME\" \"seti_letter.html\") NIL NIL \"7BIT\" 6769 171 NIL NIL NIL) \"ALTERNATIVE\" (\"BOUNDARY\" \"==alternative_xad5934455aeex\") NIL NIL)\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + BodyPartMultipart multipart, broken; + BodyPartText html; + BodyPart body; + + engine.SetStream (tokenizer); + engine.QuirksMode = ImapQuirksMode.GMail; + + try { + body = await ImapUtils.ParseBodyAsync (engine, "Unexpected token: {0}", string.Empty, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing BODYSTRUCTURE failed: {ex}"); + return; + } + + var token = await engine.ReadTokenAsync (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (body, Is.InstanceOf (), "Body types did not match."); + multipart = (BodyPartMultipart) body; + + Assert.That (multipart.ContentType.IsMimeType ("multipart", "alternative"), Is.True, "outer multipart/alternative Content-Type did not match."); + Assert.That (multipart.ContentType.Parameters["boundary"], Is.EqualTo ("==alternative_xad5934455aeex"), "outer multipart/alternative boundary param did not match"); + Assert.That (multipart.BodyParts, Has.Count.EqualTo (2), "outer multipart/alternative BodyParts count does not match."); + + Assert.That (multipart.BodyParts[0], Is.InstanceOf (), "The type of the first child does not match."); + broken = (BodyPartMultipart) multipart.BodyParts[0]; + Assert.That (broken.ContentType.IsMimeType ("multipart", "alternative"), Is.True, "inner multipart/alternative Content-Type did not match."); + Assert.That (broken.ContentType.Parameters["boundary"], Is.EqualTo ("==alternative_xad5934455aeex"), "inner multipart/alternative boundary param did not match"); + Assert.That (broken.BodyParts, Is.Empty, "inner multipart/alternative BodyParts count does not match."); + + Assert.That (multipart.BodyParts[1], Is.InstanceOf (), "The type of the second child does not match."); + html = (BodyPartText) multipart.BodyParts[1]; + Assert.That (html.ContentType.IsMimeType ("text", "html"), Is.True, "text/html Content-Type did not match."); + Assert.That (html.ContentType.Charset, Is.EqualTo ("iso-8859-1"), "text/html charset parameter did not match"); + Assert.That (html.ContentType.Name, Is.EqualTo ("seti_letter.html"), "text/html name parameter did not match"); + } + } + } + } + + // This tests the work-around for issue #777 + [Test] + public void TestParseGMailBadlyFormedMultipartBodyStructure2 () + { + const string text = "(((\"TEXT\" \"PLAIN\" (\"CHARSET\" \"UTF-8\" \"DELSP\" \"yes\" \"FORMAT\" \"flowed\") NIL NIL \"BASE64\" 10418 133 NIL NIL NIL)(\"TEXT\" \"HTML\" (\"CHARSET\" \"UTF-8\") NIL NIL \"BASE64\" 34544 442 NIL NIL NIL) \"ALTERNATIVE\" (\"BOUNDARY\" \"94eb2c1cd0507723d5054c1ce6cb\") NIL NIL)(\"RELATED\" NIL (\"ATTACHMENT\" NIL) NIL)(\"RELATED\" NIL (\"ATTACHMENT\" NIL) NIL)(\"RELATED\" NIL (\"ATTACHMENT\" NIL) NIL)(\"RELATED\" NIL (\"ATTACHMENT\" NIL) NIL) \"MIXED\" (\"BOUNDARY\" \"94eb2c1cd0507723e6054c1ce6cd\") NIL NIL)\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + BodyPart body; + + engine.SetStream (tokenizer); + engine.QuirksMode = ImapQuirksMode.GMail; + + try { + body = ImapUtils.ParseBody (engine, "Unexpected token: {0}", string.Empty, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing BODYSTRUCTURE failed: {ex}"); + return; + } + + var token = engine.ReadToken (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (body, Is.InstanceOf (), "Body types did not match."); + var mixed = (BodyPartMultipart) body; + + Assert.That (mixed.ContentType.IsMimeType ("multipart", "mixed"), Is.True, "multipart/mixed Content-Type did not match."); + Assert.That (mixed.ContentType.Parameters["boundary"], Is.EqualTo ("94eb2c1cd0507723e6054c1ce6cd"), "multipart/mixed boundary param did not match"); + Assert.That (mixed.BodyParts, Has.Count.EqualTo (5), "multipart/mixed BodyParts count does not match."); + + Assert.That (mixed.BodyParts[0], Is.InstanceOf (), "The type of the first child does not match."); + var alternative = (BodyPartMultipart) mixed.BodyParts[0]; + Assert.That (alternative.ContentType.IsMimeType ("multipart", "alternative"), Is.True, "multipart/alternative Content-Type did not match."); + Assert.That (alternative.ContentType.Parameters["boundary"], Is.EqualTo ("94eb2c1cd0507723d5054c1ce6cb"), "multipart/alternative boundary param did not match"); + Assert.That (alternative.BodyParts, Has.Count.EqualTo (2), "multipart/alternative BodyParts count does not match."); + + Assert.That (alternative.BodyParts[0], Is.InstanceOf (), "The type of the second child does not match."); + var plain = (BodyPartText) alternative.BodyParts[0]; + Assert.That (plain.ContentType.IsMimeType ("text", "plain"), Is.True, "text/plain Content-Type did not match."); + Assert.That (plain.ContentType.Charset, Is.EqualTo ("UTF-8"), "text/plain charset parameter did not match"); + Assert.That (plain.ContentType.Format, Is.EqualTo ("flowed"), "text/plain format parameter did not match"); + Assert.That (plain.ContentType.Parameters["delsp"], Is.EqualTo ("yes"), "text/plain delsp parameter did not match"); + Assert.That (plain.ContentTransferEncoding, Is.EqualTo ("BASE64"), "text/plain Content-Transfer-Encoding did not match"); + Assert.That (plain.Octets, Is.EqualTo (10418), "text/plain Octets do not match"); + Assert.That (plain.Lines, Is.EqualTo (133), "text/plain Lines don't match"); + + Assert.That (alternative.BodyParts[1], Is.InstanceOf (), "The type of the second child does not match."); + var html = (BodyPartText) alternative.BodyParts[1]; + Assert.That (html.ContentType.IsMimeType ("text", "html"), Is.True, "text/html Content-Type did not match."); + Assert.That (html.ContentType.Charset, Is.EqualTo ("UTF-8"), "text/html charset parameter did not match"); + Assert.That (html.ContentTransferEncoding, Is.EqualTo ("BASE64"), "text/phtml Content-Transfer-Encoding did not match"); + Assert.That (html.Octets, Is.EqualTo (34544), "text/html Octets do not match"); + Assert.That (html.Lines, Is.EqualTo (442), "text/html Lines don't match"); + + Assert.That (mixed.BodyParts[1], Is.InstanceOf (), "The type of the second child does not match."); + var broken1 = (BodyPartMultipart) mixed.BodyParts[1]; + Assert.That (broken1.ContentType.IsMimeType ("multipart", "related"), Is.True, "multipart/related Content-Type did not match."); + Assert.That (broken1.BodyParts, Is.Empty, "multipart/related BodyParts count does not match."); + + Assert.That (mixed.BodyParts[2], Is.InstanceOf (), "The type of the third child does not match."); + var broken2 = (BodyPartMultipart) mixed.BodyParts[2]; + Assert.That (broken2.ContentType.IsMimeType ("multipart", "related"), Is.True, "multipart/related Content-Type did not match."); + Assert.That (broken2.BodyParts, Is.Empty, "multipart/related BodyParts count does not match."); + + Assert.That (mixed.BodyParts[3], Is.InstanceOf (), "The type of the fourth child does not match."); + var broken3 = (BodyPartMultipart) mixed.BodyParts[3]; + Assert.That (broken3.ContentType.IsMimeType ("multipart", "related"), Is.True, "multipart/related Content-Type did not match."); + Assert.That (broken3.BodyParts, Is.Empty, "multipart/related BodyParts count does not match."); + + Assert.That (mixed.BodyParts[4], Is.InstanceOf (), "The type of the fifth child does not match."); + var broken4 = (BodyPartMultipart) mixed.BodyParts[4]; + Assert.That (broken4.ContentType.IsMimeType ("multipart", "related"), Is.True, "multipart/related Content-Type did not match."); + Assert.That (broken4.BodyParts, Is.Empty, "multipart/related BodyParts count does not match."); + } + } + } + } + + // This tests the work-around for issue #777 + [Test] + public async Task TestParseGMailBadlyFormedMultipartBodyStructure2Async () + { + const string text = "(((\"TEXT\" \"PLAIN\" (\"CHARSET\" \"UTF-8\" \"DELSP\" \"yes\" \"FORMAT\" \"flowed\") NIL NIL \"BASE64\" 10418 133 NIL NIL NIL)(\"TEXT\" \"HTML\" (\"CHARSET\" \"UTF-8\") NIL NIL \"BASE64\" 34544 442 NIL NIL NIL) \"ALTERNATIVE\" (\"BOUNDARY\" \"94eb2c1cd0507723d5054c1ce6cb\") NIL NIL)(\"RELATED\" NIL (\"ATTACHMENT\" NIL) NIL)(\"RELATED\" NIL (\"ATTACHMENT\" NIL) NIL)(\"RELATED\" NIL (\"ATTACHMENT\" NIL) NIL)(\"RELATED\" NIL (\"ATTACHMENT\" NIL) NIL) \"MIXED\" (\"BOUNDARY\" \"94eb2c1cd0507723e6054c1ce6cd\") NIL NIL)\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + BodyPart body; + + engine.SetStream (tokenizer); + engine.QuirksMode = ImapQuirksMode.GMail; + + try { + body = await ImapUtils.ParseBodyAsync (engine, "Unexpected token: {0}", string.Empty, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing BODYSTRUCTURE failed: {ex}"); + return; + } + + var token = await engine.ReadTokenAsync (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (body, Is.InstanceOf (), "Body types did not match."); + var mixed = (BodyPartMultipart) body; + + Assert.That (mixed.ContentType.IsMimeType ("multipart", "mixed"), Is.True, "multipart/mixed Content-Type did not match."); + Assert.That (mixed.ContentType.Parameters["boundary"], Is.EqualTo ("94eb2c1cd0507723e6054c1ce6cd"), "multipart/mixed boundary param did not match"); + Assert.That (mixed.BodyParts, Has.Count.EqualTo (5), "multipart/mixed BodyParts count does not match."); + + Assert.That (mixed.BodyParts[0], Is.InstanceOf (), "The type of the first child does not match."); + var alternative = (BodyPartMultipart) mixed.BodyParts[0]; + Assert.That (alternative.ContentType.IsMimeType ("multipart", "alternative"), Is.True, "multipart/alternative Content-Type did not match."); + Assert.That (alternative.ContentType.Parameters["boundary"], Is.EqualTo ("94eb2c1cd0507723d5054c1ce6cb"), "multipart/alternative boundary param did not match"); + Assert.That (alternative.BodyParts, Has.Count.EqualTo (2), "multipart/alternative BodyParts count does not match."); + + Assert.That (alternative.BodyParts[0], Is.InstanceOf (), "The type of the second child does not match."); + var plain = (BodyPartText) alternative.BodyParts[0]; + Assert.That (plain.ContentType.IsMimeType ("text", "plain"), Is.True, "text/plain Content-Type did not match."); + Assert.That (plain.ContentType.Charset, Is.EqualTo ("UTF-8"), "text/plain charset parameter did not match"); + Assert.That (plain.ContentType.Format, Is.EqualTo ("flowed"), "text/plain format parameter did not match"); + Assert.That (plain.ContentType.Parameters["delsp"], Is.EqualTo ("yes"), "text/plain delsp parameter did not match"); + Assert.That (plain.ContentTransferEncoding, Is.EqualTo ("BASE64"), "text/plain Content-Transfer-Encoding did not match"); + Assert.That (plain.Octets, Is.EqualTo (10418), "text/plain Octets do not match"); + Assert.That (plain.Lines, Is.EqualTo (133), "text/plain Lines don't match"); + + Assert.That (alternative.BodyParts[1], Is.InstanceOf (), "The type of the second child does not match."); + var html = (BodyPartText) alternative.BodyParts[1]; + Assert.That (html.ContentType.IsMimeType ("text", "html"), Is.True, "text/html Content-Type did not match."); + Assert.That (html.ContentType.Charset, Is.EqualTo ("UTF-8"), "text/html charset parameter did not match"); + Assert.That (html.ContentTransferEncoding, Is.EqualTo ("BASE64"), "text/phtml Content-Transfer-Encoding did not match"); + Assert.That (html.Octets, Is.EqualTo (34544), "text/html Octets do not match"); + Assert.That (html.Lines, Is.EqualTo (442), "text/html Lines don't match"); + + Assert.That (mixed.BodyParts[1], Is.InstanceOf (), "The type of the second child does not match."); + var broken1 = (BodyPartMultipart) mixed.BodyParts[1]; + Assert.That (broken1.ContentType.IsMimeType ("multipart", "related"), Is.True, "multipart/related Content-Type did not match."); + Assert.That (broken1.BodyParts, Is.Empty, "multipart/related BodyParts count does not match."); + + Assert.That (mixed.BodyParts[2], Is.InstanceOf (), "The type of the third child does not match."); + var broken2 = (BodyPartMultipart) mixed.BodyParts[2]; + Assert.That (broken2.ContentType.IsMimeType ("multipart", "related"), Is.True, "multipart/related Content-Type did not match."); + Assert.That (broken2.BodyParts, Is.Empty, "multipart/related BodyParts count does not match."); + + Assert.That (mixed.BodyParts[3], Is.InstanceOf (), "The type of the fourth child does not match."); + var broken3 = (BodyPartMultipart) mixed.BodyParts[3]; + Assert.That (broken3.ContentType.IsMimeType ("multipart", "related"), Is.True, "multipart/related Content-Type did not match."); + Assert.That (broken3.BodyParts, Is.Empty, "multipart/related BodyParts count does not match."); + + Assert.That (mixed.BodyParts[4], Is.InstanceOf (), "The type of the fifth child does not match."); + var broken4 = (BodyPartMultipart) mixed.BodyParts[4]; + Assert.That (broken4.ContentType.IsMimeType ("multipart", "related"), Is.True, "multipart/related Content-Type did not match."); + Assert.That (broken4.BodyParts, Is.Empty, "multipart/related BodyParts count does not match."); + } + } + } + } + + // Note: This tests the work-around for issue #371 (except that the example from issue #371 is also missing body-fld-enc and body-fld-octets) + [Test] + public void TestParseBadlyFormedBodyStructureWithEmptyStringMediaType () + { + const string text = "((\"TEXT\" \"PLAIN\" (\"CHARSET\" \"windows-1251\") NIL NIL \"base64\" 356 5)( \"X-ZIP\" (\"BOUNDARY\" \"\") NIL NIL \"base64\" 4096) \"MIXED\" (\"BOUNDARY\" \"--cd49a2f5ed4ed0cbb6f9f1c7f125541f\") NIL NIL)\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + BodyPartMultipart multipart; + BodyPartBasic xzip; + BodyPartText plain; + BodyPart body; + + engine.SetStream (tokenizer); + + try { + body = ImapUtils.ParseBody (engine, "Unexpected token: {0}", string.Empty, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing BODYSTRUCTURE failed: {ex}"); + return; + } + + var token = engine.ReadToken (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (body, Is.InstanceOf (), "Body types did not match."); + multipart = (BodyPartMultipart) body; + + Assert.That (multipart.ContentType.IsMimeType ("multipart", "mixed"), Is.True, "multipart/mixed Content-Type did not match."); + Assert.That (multipart.ContentType.Parameters["boundary"], Is.EqualTo ("--cd49a2f5ed4ed0cbb6f9f1c7f125541f"), "multipart/alternative boundary param did not match"); + Assert.That (multipart.BodyParts, Has.Count.EqualTo (2), "outer multipart/alternative BodyParts count does not match."); + + Assert.That (multipart.BodyParts[0], Is.InstanceOf (), "The type of the first child does not match."); + plain = (BodyPartText) multipart.BodyParts[0]; + Assert.That (plain.ContentType.IsMimeType ("text", "plain"), Is.True, "text/plain Content-Type did not match."); + Assert.That (plain.ContentType.Charset, Is.EqualTo ("windows-1251"), "text/plain charset parameter did not match"); + Assert.That (plain.Octets, Is.EqualTo (356), "text/plain octets did not match"); + Assert.That (plain.Lines, Is.EqualTo (5), "text/plain lines did not match"); + + Assert.That (multipart.BodyParts[1], Is.InstanceOf (), "The type of the second child does not match."); + xzip = (BodyPartBasic) multipart.BodyParts[1]; + Assert.That (xzip.ContentType.IsMimeType ("application", "x-zip"), Is.True, "x-zip Content-Type did not match."); + Assert.That (xzip.ContentType.Parameters["boundary"], Is.EqualTo (""), "x-zip boundary parameter did not match"); + Assert.That (xzip.Octets, Is.EqualTo (4096), "x-zip octets did not match"); + } + } + } + } + + // Note: This tests the work-around for issue #371 (except that the example from issue #371 is also missing body-fld-enc and body-fld-octets) + [Test] + public async Task TestParseBadlyFormedBodyStructureWithEmptyStringMediaTypeAsync () + { + const string text = "((\"TEXT\" \"PLAIN\" (\"CHARSET\" \"windows-1251\") NIL NIL \"base64\" 356 5)( \"X-ZIP\" (\"BOUNDARY\" \"\") NIL NIL \"base64\" 4096) \"MIXED\" (\"BOUNDARY\" \"--cd49a2f5ed4ed0cbb6f9f1c7f125541f\") NIL NIL)\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + BodyPartMultipart multipart; + BodyPartBasic xzip; + BodyPartText plain; + BodyPart body; + + engine.SetStream (tokenizer); + + try { + body = await ImapUtils.ParseBodyAsync (engine, "Unexpected token: {0}", string.Empty, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing BODYSTRUCTURE failed: {ex}"); + return; + } + + var token = await engine.ReadTokenAsync (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (body, Is.InstanceOf (), "Body types did not match."); + multipart = (BodyPartMultipart) body; + + Assert.That (multipart.ContentType.IsMimeType ("multipart", "mixed"), Is.True, "multipart/mixed Content-Type did not match."); + Assert.That (multipart.ContentType.Parameters["boundary"], Is.EqualTo ("--cd49a2f5ed4ed0cbb6f9f1c7f125541f"), "multipart/alternative boundary param did not match"); + Assert.That (multipart.BodyParts, Has.Count.EqualTo (2), "outer multipart/alternative BodyParts count does not match."); + + Assert.That (multipart.BodyParts[0], Is.InstanceOf (), "The type of the first child does not match."); + plain = (BodyPartText) multipart.BodyParts[0]; + Assert.That (plain.ContentType.IsMimeType ("text", "plain"), Is.True, "text/plain Content-Type did not match."); + Assert.That (plain.ContentType.Charset, Is.EqualTo ("windows-1251"), "text/plain charset parameter did not match"); + Assert.That (plain.Octets, Is.EqualTo (356), "text/plain octets did not match"); + Assert.That (plain.Lines, Is.EqualTo (5), "text/plain lines did not match"); + + Assert.That (multipart.BodyParts[1], Is.InstanceOf (), "The type of the second child does not match."); + xzip = (BodyPartBasic) multipart.BodyParts[1]; + Assert.That (xzip.ContentType.IsMimeType ("application", "x-zip"), Is.True, "x-zip Content-Type did not match."); + Assert.That (xzip.ContentType.Parameters["boundary"], Is.EqualTo (""), "x-zip boundary parameter did not match"); + Assert.That (xzip.Octets, Is.EqualTo (4096), "x-zip octets did not match"); + } + } + } + } + + [Test] + public void TestParseBadlyFormedBodyStructureWithMissingMediaSubtypeApplication () + { + const string text = "((\"APPLICATION\" NIL NIL NIL \"base64\" 356) \"MIXED\" (\"BOUNDARY\" \"--cd49a2f5ed4ed0cbb6f9f1c7f125541f\") NIL NIL)\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + BodyPartMultipart multipart; + BodyPartBasic basic; + BodyPart body; + + engine.SetStream (tokenizer); + + try { + body = ImapUtils.ParseBody (engine, "Unexpected token: {0}", string.Empty, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing BODYSTRUCTURE failed: {ex}"); + return; + } + + var token = engine.ReadToken (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (body, Is.InstanceOf (), "Body types did not match."); + multipart = (BodyPartMultipart) body; + + Assert.That (multipart.ContentType.IsMimeType ("multipart", "mixed"), Is.True, "multipart/mixed Content-Type did not match."); + Assert.That (multipart.ContentType.Parameters["boundary"], Is.EqualTo ("--cd49a2f5ed4ed0cbb6f9f1c7f125541f"), "multipart/alternative boundary param did not match"); + Assert.That (multipart.BodyParts, Has.Count.EqualTo (1), "outer multipart/alternative BodyParts count does not match."); + + Assert.That (multipart.BodyParts[0], Is.InstanceOf (), "The type of the first child does not match."); + basic = (BodyPartBasic) multipart.BodyParts[0]; + Assert.That (basic.ContentType.IsMimeType ("application", "octet-stream"), Is.True, "application/octet-stream Content-Type did not match."); + Assert.That (basic.Octets, Is.EqualTo (356), "application/octet-stream octets did not match"); + } + } + } + } + + [Test] + public async Task TestParseBadlyFormedBodyStructureWithMissingMediaSubtypeApplicationAsync () + { + const string text = "((\"APPLICATION\" NIL NIL NIL \"base64\" 356) \"MIXED\" (\"BOUNDARY\" \"--cd49a2f5ed4ed0cbb6f9f1c7f125541f\") NIL NIL)\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + BodyPartMultipart multipart; + BodyPartBasic basic; + BodyPart body; + + engine.SetStream (tokenizer); + + try { + body = await ImapUtils.ParseBodyAsync (engine, "Unexpected token: {0}", string.Empty, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing BODYSTRUCTURE failed: {ex}"); + return; + } + + var token = await engine.ReadTokenAsync (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (body, Is.InstanceOf (), "Body types did not match."); + multipart = (BodyPartMultipart) body; + + Assert.That (multipart.ContentType.IsMimeType ("multipart", "mixed"), Is.True, "multipart/mixed Content-Type did not match."); + Assert.That (multipart.ContentType.Parameters["boundary"], Is.EqualTo ("--cd49a2f5ed4ed0cbb6f9f1c7f125541f"), "multipart/alternative boundary param did not match"); + Assert.That (multipart.BodyParts, Has.Count.EqualTo (1), "outer multipart/alternative BodyParts count does not match."); + + Assert.That (multipart.BodyParts[0], Is.InstanceOf (), "The type of the first child does not match."); + basic = (BodyPartBasic) multipart.BodyParts[0]; + Assert.That (basic.ContentType.IsMimeType ("application", "octet-stream"), Is.True, "application/octet-stream Content-Type did not match."); + Assert.That (basic.Octets, Is.EqualTo (356), "application/octet-stream octets did not match"); + } + } + } + } + + [Test] + public void TestParseBadlyFormedBodyStructureWithMissingMediaSubtypeAudio () + { + const string text = "((\"AUDIO\" NIL NIL NIL \"base64\" 356) \"MIXED\" (\"BOUNDARY\" \"--cd49a2f5ed4ed0cbb6f9f1c7f125541f\") NIL NIL)\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + BodyPartMultipart multipart; + BodyPartBasic basic; + BodyPart body; + + engine.SetStream (tokenizer); + + try { + body = ImapUtils.ParseBody (engine, "Unexpected token: {0}", string.Empty, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing BODYSTRUCTURE failed: {ex}"); + return; + } + + var token = engine.ReadToken (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (body, Is.InstanceOf (), "Body types did not match."); + multipart = (BodyPartMultipart) body; + + Assert.That (multipart.ContentType.IsMimeType ("multipart", "mixed"), Is.True, "multipart/mixed Content-Type did not match."); + Assert.That (multipart.ContentType.Parameters["boundary"], Is.EqualTo ("--cd49a2f5ed4ed0cbb6f9f1c7f125541f"), "multipart/alternative boundary param did not match"); + Assert.That (multipart.BodyParts, Has.Count.EqualTo (1), "outer multipart/alternative BodyParts count does not match."); + + Assert.That (multipart.BodyParts[0], Is.InstanceOf (), "The type of the first child does not match."); + basic = (BodyPartBasic) multipart.BodyParts[0]; + Assert.That (basic.ContentType.IsMimeType ("application", "audio"), Is.True, "application/audio Content-Type did not match."); + Assert.That (basic.Octets, Is.EqualTo (356), "application/audio octets did not match"); + } + } + } + } + + [Test] + public async Task TestParseBadlyFormedBodyStructureWithMissingMediaSubtypeAudioAsync () + { + const string text = "((\"AUDIO\" NIL NIL NIL \"base64\" 356) \"MIXED\" (\"BOUNDARY\" \"--cd49a2f5ed4ed0cbb6f9f1c7f125541f\") NIL NIL)\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + BodyPartMultipart multipart; + BodyPartBasic basic; + BodyPart body; + + engine.SetStream (tokenizer); + + try { + body = await ImapUtils.ParseBodyAsync (engine, "Unexpected token: {0}", string.Empty, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing BODYSTRUCTURE failed: {ex}"); + return; + } + + var token = await engine.ReadTokenAsync (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (body, Is.InstanceOf (), "Body types did not match."); + multipart = (BodyPartMultipart) body; + + Assert.That (multipart.ContentType.IsMimeType ("multipart", "mixed"), Is.True, "multipart/mixed Content-Type did not match."); + Assert.That (multipart.ContentType.Parameters["boundary"], Is.EqualTo ("--cd49a2f5ed4ed0cbb6f9f1c7f125541f"), "multipart/alternative boundary param did not match"); + Assert.That (multipart.BodyParts, Has.Count.EqualTo (1), "outer multipart/alternative BodyParts count does not match."); + + Assert.That (multipart.BodyParts[0], Is.InstanceOf (), "The type of the first child does not match."); + basic = (BodyPartBasic) multipart.BodyParts[0]; + Assert.That (basic.ContentType.IsMimeType ("application", "audio"), Is.True, "application/audio Content-Type did not match."); + Assert.That (basic.Octets, Is.EqualTo (356), "application/audio octets did not match"); + } + } + } + } + + [Test] + public void TestParseBadlyFormedBodyStructureWithMissingMediaSubtypeMessage () + { + const string text = "((\"MESSAGE\" NIL NIL NIL \"base64\" 356) \"MIXED\" (\"BOUNDARY\" \"--cd49a2f5ed4ed0cbb6f9f1c7f125541f\") NIL NIL)\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + BodyPartMultipart multipart; + BodyPartBasic basic; + BodyPart body; + + engine.SetStream (tokenizer); + + try { + body = ImapUtils.ParseBody (engine, "Unexpected token: {0}", string.Empty, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing BODYSTRUCTURE failed: {ex}"); + return; + } + + var token = engine.ReadToken (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (body, Is.InstanceOf (), "Body types did not match."); + multipart = (BodyPartMultipart) body; + + Assert.That (multipart.ContentType.IsMimeType ("multipart", "mixed"), Is.True, "multipart/mixed Content-Type did not match."); + Assert.That (multipart.ContentType.Parameters["boundary"], Is.EqualTo ("--cd49a2f5ed4ed0cbb6f9f1c7f125541f"), "multipart/alternative boundary param did not match"); + Assert.That (multipart.BodyParts, Has.Count.EqualTo (1), "outer multipart/alternative BodyParts count does not match."); + + Assert.That (multipart.BodyParts[0], Is.InstanceOf (), "The type of the first child does not match."); + basic = (BodyPartBasic) multipart.BodyParts[0]; + Assert.That (basic.ContentType.IsMimeType ("application", "message"), Is.True, "application/message Content-Type did not match."); + Assert.That (basic.Octets, Is.EqualTo (356), "application/message octets did not match"); + } + } + } + } + + [Test] + public async Task TestParseBadlyFormedBodyStructureWithMissingMediaSubtypeMessageAsync () + { + const string text = "((\"MESSAGE\" NIL NIL NIL \"base64\" 356) \"MIXED\" (\"BOUNDARY\" \"--cd49a2f5ed4ed0cbb6f9f1c7f125541f\") NIL NIL)\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + BodyPartMultipart multipart; + BodyPartBasic basic; + BodyPart body; + + engine.SetStream (tokenizer); + + try { + body = await ImapUtils.ParseBodyAsync (engine, "Unexpected token: {0}", string.Empty, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing BODYSTRUCTURE failed: {ex}"); + return; + } + + var token = await engine.ReadTokenAsync (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (body, Is.InstanceOf (), "Body types did not match."); + multipart = (BodyPartMultipart) body; + + Assert.That (multipart.ContentType.IsMimeType ("multipart", "mixed"), Is.True, "multipart/mixed Content-Type did not match."); + Assert.That (multipart.ContentType.Parameters["boundary"], Is.EqualTo ("--cd49a2f5ed4ed0cbb6f9f1c7f125541f"), "multipart/alternative boundary param did not match"); + Assert.That (multipart.BodyParts, Has.Count.EqualTo (1), "outer multipart/alternative BodyParts count does not match."); + + Assert.That (multipart.BodyParts[0], Is.InstanceOf (), "The type of the first child does not match."); + basic = (BodyPartBasic) multipart.BodyParts[0]; + Assert.That (basic.ContentType.IsMimeType ("application", "message"), Is.True, "application/message Content-Type did not match."); + Assert.That (basic.Octets, Is.EqualTo (356), "application/message octets did not match"); + } + } + } + } + + [Test] + public void TestParseBadlyFormedBodyStructureWithMissingMediaSubtypeText () + { + const string text = "((\"TEXT\" (\"CHARSET\" \"windows-1251\") NIL NIL \"base64\" 356 5) \"MIXED\" (\"BOUNDARY\" \"--cd49a2f5ed4ed0cbb6f9f1c7f125541f\") NIL NIL)\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + BodyPartMultipart multipart; + BodyPartText plain; + BodyPart body; + + engine.SetStream (tokenizer); + + try { + body = ImapUtils.ParseBody (engine, "Unexpected token: {0}", string.Empty, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing BODYSTRUCTURE failed: {ex}"); + return; + } + + var token = engine.ReadToken (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (body, Is.InstanceOf (), "Body types did not match."); + multipart = (BodyPartMultipart) body; + + Assert.That (multipart.ContentType.IsMimeType ("multipart", "mixed"), Is.True, "multipart/mixed Content-Type did not match."); + Assert.That (multipart.ContentType.Parameters["boundary"], Is.EqualTo ("--cd49a2f5ed4ed0cbb6f9f1c7f125541f"), "multipart/alternative boundary param did not match"); + Assert.That (multipart.BodyParts, Has.Count.EqualTo (1), "outer multipart/alternative BodyParts count does not match."); + + Assert.That (multipart.BodyParts[0], Is.InstanceOf (), "The type of the first child does not match."); + plain = (BodyPartText) multipart.BodyParts[0]; + Assert.That (plain.ContentType.IsMimeType ("text", "plain"), Is.True, "text/plain Content-Type did not match."); + Assert.That (plain.ContentType.Charset, Is.EqualTo ("windows-1251"), "text/plain charset parameter did not match"); + Assert.That (plain.Octets, Is.EqualTo (356), "text/plain octets did not match"); + Assert.That (plain.Lines, Is.EqualTo (5), "text/plain lines did not match"); + } + } + } + } + + [Test] + public async Task TestParseBadlyFormedBodyStructureWithMissingMediaSubtypeTextAsync () + { + const string text = "((\"TEXT\" (\"CHARSET\" \"windows-1251\") NIL NIL \"base64\" 356 5) \"MIXED\" (\"BOUNDARY\" \"--cd49a2f5ed4ed0cbb6f9f1c7f125541f\") NIL NIL)\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + BodyPartMultipart multipart; + BodyPartText plain; + BodyPart body; + + engine.SetStream (tokenizer); + + try { + body = await ImapUtils.ParseBodyAsync (engine, "Unexpected token: {0}", string.Empty, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing BODYSTRUCTURE failed: {ex}"); + return; + } + + var token = await engine.ReadTokenAsync (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (body, Is.InstanceOf (), "Body types did not match."); + multipart = (BodyPartMultipart) body; + + Assert.That (multipart.ContentType.IsMimeType ("multipart", "mixed"), Is.True, "multipart/mixed Content-Type did not match."); + Assert.That (multipart.ContentType.Parameters["boundary"], Is.EqualTo ("--cd49a2f5ed4ed0cbb6f9f1c7f125541f"), "multipart/alternative boundary param did not match"); + Assert.That (multipart.BodyParts, Has.Count.EqualTo (1), "outer multipart/alternative BodyParts count does not match."); + + Assert.That (multipart.BodyParts[0], Is.InstanceOf (), "The type of the first child does not match."); + plain = (BodyPartText) multipart.BodyParts[0]; + Assert.That (plain.ContentType.IsMimeType ("text", "plain"), Is.True, "text/plain Content-Type did not match."); + Assert.That (plain.ContentType.Charset, Is.EqualTo ("windows-1251"), "text/plain charset parameter did not match"); + Assert.That (plain.Octets, Is.EqualTo (356), "text/plain octets did not match"); + Assert.That (plain.Lines, Is.EqualTo (5), "text/plain lines did not match"); + } + } + } + } + + // Note: This tests the work-around for issue #485 + [Test] + public void TestParseBadlyQuotedBodyStructure () + { + const string text = "((\"MOUNDARY=\"_006_5DBB50A5A54730AD4A54730AD4A54730AD4A54730AD42KOS_\"\" \"OCTET-STREAM\" (\"name\" \"test.dat\") NIL NIL \"quoted-printable\" 383137 NIL (\"attachment\" (\"filename\" \"test.dat\")))(\"MOUNDARY=\"_006_5DBB50A5D3ABEC4E85A03EAD527CA5474B3D0AF9E6EXMBXSVR02KOS_\"\" \"OCTET-STREAM\" (\"name\" \"test.dat\") NIL NIL \"quoted-printable\" 383137 NIL (\"attachment\" (\"filename\" \"test.dat\"))) \"MIXED\" (\"boundary\" \"----=_NextPart_000_730AD4A547.730AD4A547F40\"))\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + BodyPartMultipart multipart; + BodyPartBasic basic; + BodyPart body; + + engine.SetStream (tokenizer); + + try { + body = ImapUtils.ParseBody (engine, "Unexpected token: {0}", string.Empty, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing BODYSTRUCTURE failed: {ex}"); + return; + } + + var token = engine.ReadToken (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (body, Is.InstanceOf (), "Body types did not match."); + multipart = (BodyPartMultipart) body; + + Assert.That (body.ContentType.IsMimeType ("multipart", "mixed"), Is.True, "Content-Type did not match."); + Assert.That (body.ContentType.Parameters ["boundary"], Is.EqualTo ("----=_NextPart_000_730AD4A547.730AD4A547F40"), "boundary param did not match"); + Assert.That (multipart.BodyParts, Has.Count.EqualTo (2), "BodyParts count does not match."); + + Assert.That (multipart.BodyParts[0], Is.InstanceOf (), "The type of the first child does not match."); + basic = (BodyPartBasic) multipart.BodyParts[0]; + Assert.That (basic.ContentType.MediaType, Is.EqualTo ("MOUNDARY=\"_006_5DBB50A5A54730AD4A54730AD4A54730AD4A54730AD42KOS_\""), "ContentType.MediaType does not match for first child."); + + Assert.That (multipart.BodyParts[1], Is.InstanceOf (), "The type of the second child does not match."); + basic = (BodyPartBasic) multipart.BodyParts[1]; + Assert.That (basic.ContentType.MediaType, Is.EqualTo ("MOUNDARY=\"_006_5DBB50A5D3ABEC4E85A03EAD527CA5474B3D0AF9E6EXMBXSVR02KOS_\""), "ContentType.MediaType does not match for second child."); + } + } + } + } + + // Note: This tests the work-around for issue #485 + [Test] + public async Task TestParseBadlyQuotedBodyStructureAsync () + { + const string text = "((\"MOUNDARY=\"_006_5DBB50A5A54730AD4A54730AD4A54730AD4A54730AD42KOS_\"\" \"OCTET-STREAM\" (\"name\" \"test.dat\") NIL NIL \"quoted-printable\" 383137 NIL (\"attachment\" (\"filename\" \"test.dat\")))(\"MOUNDARY=\"_006_5DBB50A5D3ABEC4E85A03EAD527CA5474B3D0AF9E6EXMBXSVR02KOS_\"\" \"OCTET-STREAM\" (\"name\" \"test.dat\") NIL NIL \"quoted-printable\" 383137 NIL (\"attachment\" (\"filename\" \"test.dat\"))) \"MIXED\" (\"boundary\" \"----=_NextPart_000_730AD4A547.730AD4A547F40\"))\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + BodyPartMultipart multipart; + BodyPartBasic basic; + BodyPart body; + + engine.SetStream (tokenizer); + + try { + body = await ImapUtils.ParseBodyAsync (engine, "Unexpected token: {0}", string.Empty, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing BODYSTRUCTURE failed: {ex}"); + return; + } + + var token = await engine.ReadTokenAsync (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (body, Is.InstanceOf (), "Body types did not match."); + multipart = (BodyPartMultipart) body; + + Assert.That (body.ContentType.IsMimeType ("multipart", "mixed"), Is.True, "Content-Type did not match."); + Assert.That (body.ContentType.Parameters["boundary"], Is.EqualTo ("----=_NextPart_000_730AD4A547.730AD4A547F40"), "boundary param did not match"); + Assert.That (multipart.BodyParts, Has.Count.EqualTo (2), "BodyParts count does not match."); + + Assert.That (multipart.BodyParts[0], Is.InstanceOf (), "The type of the first child does not match."); + basic = (BodyPartBasic) multipart.BodyParts[0]; + Assert.That (basic.ContentType.MediaType, Is.EqualTo ("MOUNDARY=\"_006_5DBB50A5A54730AD4A54730AD4A54730AD4A54730AD42KOS_\""), "ContentType.MediaType does not match for first child."); + + Assert.That (multipart.BodyParts[1], Is.InstanceOf (), "The type of the second child does not match."); + basic = (BodyPartBasic) multipart.BodyParts[1]; + Assert.That (basic.ContentType.MediaType, Is.EqualTo ("MOUNDARY=\"_006_5DBB50A5D3ABEC4E85A03EAD527CA5474B3D0AF9E6EXMBXSVR02KOS_\""), "ContentType.MediaType does not match for second child."); + } + } + } + } + + [Test] + public void TestParseMultipartBodyStructureWithNilBodyFldParam () + { + const string text = "(((\"text\" \"plain\" (\"charset\" \"UTF-8\") NIL NIL \"7bit\" 148 12 NIL NIL NIL NIL)(\"text\" \"html\" (\"charset\" \"UTF-8\") NIL NIL \"quoted-printable\" 337 6 NIL NIL NIL NIL) \"alternative\" (\"boundary\" \"6c7f221bed92d80548353834d8e2\") NIL NIL NIL)((\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 0 0) \"x-zip\" NIL (\"attachment\" (\"filename\" \"YSOZ 265230.ZIP\")) NIL NIL) \"mixed\" (\"boundary\" \"c52bbfc0dd5365efa39b9f80eac3\") NIL NIL NIL)\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + BodyPartMultipart multipart, alternative, xzip; + BodyPart body; + + engine.SetStream (tokenizer); + + try { + body = ImapUtils.ParseBody (engine, "Unexpected token: {0}", string.Empty, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing BODYSTRUCTURE failed: {ex}"); + return; + } + + var token = engine.ReadToken (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (body, Is.InstanceOf (), "Body types did not match."); + multipart = (BodyPartMultipart) body; + + Assert.That (body.ContentType.IsMimeType ("multipart", "mixed"), Is.True, "Content-Type did not match."); + Assert.That (body.ContentType.Parameters["boundary"], Is.EqualTo ("c52bbfc0dd5365efa39b9f80eac3"), "boundary param did not match"); + Assert.That (multipart.BodyParts, Has.Count.EqualTo (2), "BodyParts count does not match."); + + Assert.That (multipart.BodyParts[0], Is.InstanceOf (), "The type of the first child does not match."); + alternative = (BodyPartMultipart) multipart.BodyParts[0]; + Assert.That (alternative.ContentType.MediaSubtype, Is.EqualTo ("alternative"), "Content-Type did not match."); + + Assert.That (multipart.BodyParts[1], Is.InstanceOf (), "The type of the second child does not match."); + xzip = (BodyPartMultipart) multipart.BodyParts[1]; + Assert.That (xzip.ContentType.MediaSubtype, Is.EqualTo ("x-zip"), "Content-Type did not match."); + Assert.That (xzip.ContentType.Parameters, Is.Empty, "Content-Type should not have params."); + } + } + } + } + + [Test] + public async Task TestParseMultipartBodyStructureWithNilBodyFldParamAsync () + { + const string text = "(((\"text\" \"plain\" (\"charset\" \"UTF-8\") NIL NIL \"7bit\" 148 12 NIL NIL NIL NIL)(\"text\" \"html\" (\"charset\" \"UTF-8\") NIL NIL \"quoted-printable\" 337 6 NIL NIL NIL NIL) \"alternative\" (\"boundary\" \"6c7f221bed92d80548353834d8e2\") NIL NIL NIL)((\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 0 0) \"x-zip\" NIL (\"attachment\" (\"filename\" \"YSOZ 265230.ZIP\")) NIL NIL) \"mixed\" (\"boundary\" \"c52bbfc0dd5365efa39b9f80eac3\") NIL NIL NIL)\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + BodyPartMultipart multipart, alternative, xzip; + BodyPart body; + + engine.SetStream (tokenizer); + + try { + body = await ImapUtils.ParseBodyAsync (engine, "Unexpected token: {0}", string.Empty, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing BODYSTRUCTURE failed: {ex}"); + return; + } + + var token = await engine.ReadTokenAsync (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (body, Is.InstanceOf (), "Body types did not match."); + multipart = (BodyPartMultipart) body; + + Assert.That (body.ContentType.IsMimeType ("multipart", "mixed"), Is.True, "Content-Type did not match."); + Assert.That (body.ContentType.Parameters["boundary"], Is.EqualTo ("c52bbfc0dd5365efa39b9f80eac3"), "boundary param did not match"); + Assert.That (multipart.BodyParts, Has.Count.EqualTo (2), "BodyParts count does not match."); + + Assert.That (multipart.BodyParts[0], Is.InstanceOf (), "The type of the first child does not match."); + alternative = (BodyPartMultipart) multipart.BodyParts[0]; + Assert.That (alternative.ContentType.MediaSubtype, Is.EqualTo ("alternative"), "Content-Type did not match."); + + Assert.That (multipart.BodyParts[1], Is.InstanceOf (), "The type of the second child does not match."); + xzip = (BodyPartMultipart) multipart.BodyParts[1]; + Assert.That (xzip.ContentType.MediaSubtype, Is.EqualTo ("x-zip"), "Content-Type did not match."); + Assert.That (xzip.ContentType.Parameters, Is.Empty, "Content-Type should not have params."); + } + } + } + } + + [Test] + public void TestParseMultipartBodyStructureWithoutBodyFldDsp () + { + // Test case from https://stackoverflow.com/questions/33481604/mailkit-fetch-unexpected-token-in-imap-response-qstring-multipart-message + const string text = "((\"text\" \"plain\" (\"charset\" \"UTF-8\") NIL \"Message text\" \"Quoted-printable\" 209 6 NIL (\"inline\" NIL) NIL NIL)(\"text\" \"xml\" (\"name\" \"4441004299066.xml\") NIL \"4441004299066.xml\" \"Base64\" 10642 137 NIL (\"inline\" (\"filename\" \"4441004299066.xml\")) NIL NIL)(\"application\" \"pdf\" (\"name\" \"4441004299066.pdf\") NIL \"4441004299066.pdf\" \"Base64\" 48448 NIL (\"inline\" (\"filename\" \"4441004299066.pdf\")) NIL NIL) \"mixed\" (\"boundary\" \"6624CFB2_17170C36_Synapse_boundary\") \"Multipart message\" NIL)\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + BodyPartMultipart multipart; + BodyPartBasic basic; + BodyPart body; + + engine.SetStream (tokenizer); + + try { + body = ImapUtils.ParseBody (engine, "Unexpected token: {0}", string.Empty, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing BODYSTRUCTURE failed: {ex}"); + return; + } + + var token = engine.ReadToken (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (body, Is.InstanceOf (), "Body types did not match."); + multipart = (BodyPartMultipart) body; + + Assert.That (body.ContentType.IsMimeType ("multipart", "mixed"), Is.True, "Content-Type did not match."); + Assert.That (body.ContentType.Parameters ["boundary"], Is.EqualTo ("6624CFB2_17170C36_Synapse_boundary"), "boundary param did not match"); + Assert.That (multipart.BodyParts, Has.Count.EqualTo (3), "BodyParts count does not match."); + + Assert.That (multipart.BodyParts[0], Is.InstanceOf (), "The type of the first child does not match."); + basic = (BodyPartBasic) multipart.BodyParts[0]; + Assert.That (basic.ContentType.MediaSubtype, Is.EqualTo ("plain"), "Content-Type did not match."); + Assert.That (basic.ContentDescription, Is.EqualTo ("Message text"), "Content-Description does not match."); + + Assert.That (multipart.BodyParts[1], Is.InstanceOf (), "The type of the second child does not match."); + basic = (BodyPartBasic) multipart.BodyParts[1]; + Assert.That (basic.ContentType.MediaSubtype, Is.EqualTo ("xml"), "Content-Type did not match."); + Assert.That (basic.ContentDescription, Is.EqualTo ("4441004299066.xml"), "Content-Description does not match."); + + Assert.That (multipart.BodyParts[2], Is.InstanceOf (), "The type of the third child does not match."); + basic = (BodyPartBasic) multipart.BodyParts[2]; + Assert.That (basic.ContentType.MediaType, Is.EqualTo ("application"), "Content-Type did not match."); + Assert.That (basic.ContentType.MediaSubtype, Is.EqualTo ("pdf"), "Content-Type did not match."); + Assert.That (basic.ContentDescription, Is.EqualTo ("4441004299066.pdf"), "Content-Description does not match."); + } + } + } + } + + [Test] + public async Task TestParseMultipartBodyStructureWithoutBodyFldDspAsync () + { + // Test case from https://stackoverflow.com/questions/33481604/mailkit-fetch-unexpected-token-in-imap-response-qstring-multipart-message + const string text = "((\"text\" \"plain\" (\"charset\" \"UTF-8\") NIL \"Message text\" \"Quoted-printable\" 209 6 NIL (\"inline\" NIL) NIL NIL)(\"text\" \"xml\" (\"name\" \"4441004299066.xml\") NIL \"4441004299066.xml\" \"Base64\" 10642 137 NIL (\"inline\" (\"filename\" \"4441004299066.xml\")) NIL NIL)(\"application\" \"pdf\" (\"name\" \"4441004299066.pdf\") NIL \"4441004299066.pdf\" \"Base64\" 48448 NIL (\"inline\" (\"filename\" \"4441004299066.pdf\")) NIL NIL) \"mixed\" (\"boundary\" \"6624CFB2_17170C36_Synapse_boundary\") \"Multipart message\" NIL)\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + BodyPartMultipart multipart; + BodyPartBasic basic; + BodyPart body; + + engine.SetStream (tokenizer); + + try { + body = await ImapUtils.ParseBodyAsync (engine, "Unexpected token: {0}", string.Empty, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing BODYSTRUCTURE failed: {ex}"); + return; + } + + var token = await engine.ReadTokenAsync (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (body, Is.InstanceOf (), "Body types did not match."); + multipart = (BodyPartMultipart) body; + + Assert.That (body.ContentType.IsMimeType ("multipart", "mixed"), Is.True, "Content-Type did not match."); + Assert.That (body.ContentType.Parameters["boundary"], Is.EqualTo ("6624CFB2_17170C36_Synapse_boundary"), "boundary param did not match"); + Assert.That (multipart.BodyParts, Has.Count.EqualTo (3), "BodyParts count does not match."); + + Assert.That (multipart.BodyParts[0], Is.InstanceOf (), "The type of the first child does not match."); + basic = (BodyPartBasic) multipart.BodyParts[0]; + Assert.That (basic.ContentType.MediaSubtype, Is.EqualTo ("plain"), "Content-Type did not match."); + Assert.That (basic.ContentDescription, Is.EqualTo ("Message text"), "Content-Description does not match."); + + Assert.That (multipart.BodyParts[1], Is.InstanceOf (), "The type of the second child does not match."); + basic = (BodyPartBasic) multipart.BodyParts[1]; + Assert.That (basic.ContentType.MediaSubtype, Is.EqualTo ("xml"), "Content-Type did not match."); + Assert.That (basic.ContentDescription, Is.EqualTo ("4441004299066.xml"), "Content-Description does not match."); + + Assert.That (multipart.BodyParts[2], Is.InstanceOf (), "The type of the third child does not match."); + basic = (BodyPartBasic) multipart.BodyParts[2]; + Assert.That (basic.ContentType.MediaType, Is.EqualTo ("application"), "Content-Type did not match."); + Assert.That (basic.ContentType.MediaSubtype, Is.EqualTo ("pdf"), "Content-Type did not match."); + Assert.That (basic.ContentDescription, Is.EqualTo ("4441004299066.pdf"), "Content-Description does not match."); + } + } + } + } + + // Note: This tests the work-around for issue #919 + [Test] + public void TestParseBodyStructureWithNonParenthesizedBodyFldDsp () + { + const string text = "((\"text\" \"plain\" (\"charset\" \"ISO-8859-1\") NIL NIL \"QUOTED-PRINTABLE\" 850 31 NIL \"inline\" NIL NIL)(\"text\" \"html\" (\"charset\" \"ISO-8859-1\") NIL NIL \"QUOTED-PRINTABLE\" 14692 502 NIL \"inline\" NIL NIL) \"alternative\" (\"boundary\" \"----=_Part_45280395_786508794.1562673197246\") NIL NIL)\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + BodyPartMultipart multipart; + BodyPartText plain, html; + BodyPart body; + + engine.SetStream (tokenizer); + + try { + body = ImapUtils.ParseBody (engine, "Unexpected token: {0}", string.Empty, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing BODYSTRUCTURE failed: {ex}"); + return; + } + + var token = engine.ReadToken (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (body, Is.InstanceOf (), "Body types did not match."); + multipart = (BodyPartMultipart) body; + + Assert.That (body.ContentType.IsMimeType ("multipart", "alternative"), Is.True, "Content-Type did not match."); + Assert.That (body.ContentType.Parameters["boundary"], Is.EqualTo ("----=_Part_45280395_786508794.1562673197246"), "boundary param did not match"); + Assert.That (multipart.BodyParts, Has.Count.EqualTo (2), "BodyParts count does not match."); + + Assert.That (multipart.BodyParts[0], Is.InstanceOf (), "The type of the first child does not match."); + plain = (BodyPartText) multipart.BodyParts[0]; + Assert.That (plain.ContentType.MimeType, Is.EqualTo ("text/plain"), "Content-Type did not match."); + Assert.That (plain.ContentType.Charset, Is.EqualTo ("ISO-8859-1"), "Content-Type charset parameter did not match."); + Assert.That (plain.ContentTransferEncoding, Is.EqualTo ("QUOTED-PRINTABLE"), "Content-Transfer-Encoding did not match."); + Assert.That (plain.Octets, Is.EqualTo (850), "Octets did not match."); + Assert.That (plain.Lines, Is.EqualTo (31), "Lines did not match."); + Assert.That (plain.ContentDisposition.Disposition, Is.EqualTo ("inline"), "Content-Disposition did not match."); + + Assert.That (multipart.BodyParts[1], Is.InstanceOf (), "The type of the second child does not match."); + html = (BodyPartText) multipart.BodyParts[1]; + Assert.That (html.ContentType.MimeType, Is.EqualTo ("text/html"), "Content-Type did not match."); + Assert.That (html.ContentType.Charset, Is.EqualTo ("ISO-8859-1"), "Content-Type charset parameter did not match."); + Assert.That (html.ContentTransferEncoding, Is.EqualTo ("QUOTED-PRINTABLE"), "Content-Transfer-Encoding did not match."); + Assert.That (html.Octets, Is.EqualTo (14692), "Octets did not match."); + Assert.That (html.Lines, Is.EqualTo (502), "Lines did not match."); + Assert.That (html.ContentDisposition.Disposition, Is.EqualTo ("inline"), "Content-Disposition did not match."); + } + } + } + } + + // Note: This tests the work-around for issue #919 + [Test] + public async Task TestParseBodyStructureWithNonParenthesizedBodyFldDspAsync () + { + const string text = "((\"text\" \"plain\" (\"charset\" \"ISO-8859-1\") NIL NIL \"QUOTED-PRINTABLE\" 850 31 NIL \"inline\" NIL NIL)(\"text\" \"html\" (\"charset\" \"ISO-8859-1\") NIL NIL \"QUOTED-PRINTABLE\" 14692 502 NIL \"inline\" NIL NIL) \"alternative\" (\"boundary\" \"----=_Part_45280395_786508794.1562673197246\") NIL NIL)\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + BodyPartMultipart multipart; + BodyPartText plain, html; + BodyPart body; + + engine.SetStream (tokenizer); + + try { + body = await ImapUtils.ParseBodyAsync (engine, "Unexpected token: {0}", string.Empty, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing BODYSTRUCTURE failed: {ex}"); + return; + } + + var token = await engine.ReadTokenAsync (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (body, Is.InstanceOf (), "Body types did not match."); + multipart = (BodyPartMultipart) body; + + Assert.That (body.ContentType.IsMimeType ("multipart", "alternative"), Is.True, "Content-Type did not match."); + Assert.That (body.ContentType.Parameters["boundary"], Is.EqualTo ("----=_Part_45280395_786508794.1562673197246"), "boundary param did not match"); + Assert.That (multipart.BodyParts, Has.Count.EqualTo (2), "BodyParts count does not match."); + + Assert.That (multipart.BodyParts[0], Is.InstanceOf (), "The type of the first child does not match."); + plain = (BodyPartText) multipart.BodyParts[0]; + Assert.That (plain.ContentType.MimeType, Is.EqualTo ("text/plain"), "Content-Type did not match."); + Assert.That (plain.ContentType.Charset, Is.EqualTo ("ISO-8859-1"), "Content-Type charset parameter did not match."); + Assert.That (plain.ContentTransferEncoding, Is.EqualTo ("QUOTED-PRINTABLE"), "Content-Transfer-Encoding did not match."); + Assert.That (plain.Octets, Is.EqualTo (850), "Octets did not match."); + Assert.That (plain.Lines, Is.EqualTo (31), "Lines did not match."); + Assert.That (plain.ContentDisposition.Disposition, Is.EqualTo ("inline"), "Content-Disposition did not match."); + + Assert.That (multipart.BodyParts[1], Is.InstanceOf (), "The type of the second child does not match."); + html = (BodyPartText) multipart.BodyParts[1]; + Assert.That (html.ContentType.MimeType, Is.EqualTo ("text/html"), "Content-Type did not match."); + Assert.That (html.ContentType.Charset, Is.EqualTo ("ISO-8859-1"), "Content-Type charset parameter did not match."); + Assert.That (html.ContentTransferEncoding, Is.EqualTo ("QUOTED-PRINTABLE"), "Content-Transfer-Encoding did not match."); + Assert.That (html.Octets, Is.EqualTo (14692), "Octets did not match."); + Assert.That (html.Lines, Is.EqualTo (502), "Lines did not match."); + Assert.That (html.ContentDisposition.Disposition, Is.EqualTo ("inline"), "Content-Disposition did not match."); + } + } + } + } + + // Note: This tests the work-around for an Exchange bug + [Test] + public void TestParseBodyStructureWithNilNilBodyFldDsp () + { + const string text = "((\"text\" \"plain\" (\"charset\" \"iso-8859-1\") NIL \"Mail message body\" \"quoted-printable\" 2201 34 NIL NIL NIL NIL)(\"application\" \"msword\" NIL NIL NIL \"base64\" 50446 NIL (NIL NIL) NIL NIL)(\"application\" \"msword\" NIL NIL NIL \"base64\" 45544 NIL (\"attachment\" (\"filename\" \"PREIS ANSPRUCHS FORMULAR.doc\")) NIL NIL) \"mixed\" (\"boundary\" \"===============1176586998==\") NIL NIL)\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + BodyPartMultipart multipart; + BodyPartBasic msword; + BodyPartText plain; + BodyPart body; + + engine.SetStream (tokenizer); + + try { + body = ImapUtils.ParseBody (engine, "Unexpected token: {0}", string.Empty, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing BODYSTRUCTURE failed: {ex}"); + return; + } + + var token = engine.ReadToken (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (body, Is.InstanceOf (), "Body types did not match."); + multipart = (BodyPartMultipart) body; + + Assert.That (body.ContentType.IsMimeType ("multipart", "mixed"), Is.True, "Content-Type did not match."); + Assert.That (body.ContentType.Parameters["boundary"], Is.EqualTo ("===============1176586998=="), "boundary param did not match"); + Assert.That (multipart.BodyParts, Has.Count.EqualTo (3), "BodyParts count does not match."); + + Assert.That (multipart.BodyParts[0], Is.InstanceOf (), "The type of the first child does not match."); + plain = (BodyPartText) multipart.BodyParts[0]; + Assert.That (plain.ContentType.MimeType, Is.EqualTo ("text/plain"), "Content-Type did not match."); + Assert.That (plain.ContentType.Charset, Is.EqualTo ("iso-8859-1"), "Content-Type charset parameter did not match."); + Assert.That (plain.ContentTransferEncoding, Is.EqualTo ("quoted-printable"), "Content-Transfer-Encoding did not match."); + Assert.That (plain.ContentDescription, Is.EqualTo ("Mail message body"), "Content-Description did not match."); + Assert.That (plain.Octets, Is.EqualTo (2201), "Octets did not match."); + Assert.That (plain.Lines, Is.EqualTo (34), "Lines did not match."); + Assert.That (plain.ContentDisposition, Is.Null, "Content-Disposition did not match."); + + Assert.That (multipart.BodyParts[1], Is.InstanceOf (), "The type of the second child does not match."); + msword = (BodyPartBasic) multipart.BodyParts[1]; + Assert.That (msword.ContentType.MimeType, Is.EqualTo ("application/msword"), "Content-Type did not match."); + Assert.That (msword.ContentTransferEncoding, Is.EqualTo ("base64"), "Content-Transfer-Encoding did not match."); + Assert.That (msword.Octets, Is.EqualTo (50446), "Octets did not match."); + Assert.That (msword.ContentDisposition, Is.Null, "Content-Disposition did not match."); + + Assert.That (multipart.BodyParts[2], Is.InstanceOf (), "The type of the second child does not match."); + msword = (BodyPartBasic) multipart.BodyParts[2]; + Assert.That (msword.ContentType.MimeType, Is.EqualTo ("application/msword"), "Content-Type did not match."); + Assert.That (msword.ContentTransferEncoding, Is.EqualTo ("base64"), "Content-Transfer-Encoding did not match."); + Assert.That (msword.Octets, Is.EqualTo (45544), "Octets did not match."); + Assert.That (msword.ContentDisposition.Disposition, Is.EqualTo ("attachment"), "Content-Disposition did not match."); + Assert.That (msword.ContentDisposition.FileName, Is.EqualTo ("PREIS ANSPRUCHS FORMULAR.doc"), "Filename parameters do not match."); + } + } + } + } + + // Note: This tests the work-around for an Exchange bug + [Test] + public async Task TestParseBodyStructureWithNilNilBodyFldDspAsync () + { + const string text = "((\"text\" \"plain\" (\"charset\" \"iso-8859-1\") NIL \"Mail message body\" \"quoted-printable\" 2201 34 NIL NIL NIL NIL)(\"application\" \"msword\" NIL NIL NIL \"base64\" 50446 NIL (NIL NIL) NIL NIL)(\"application\" \"msword\" NIL NIL NIL \"base64\" 45544 NIL (\"attachment\" (\"filename\" \"PREIS ANSPRUCHS FORMULAR.doc\")) NIL NIL) \"mixed\" (\"boundary\" \"===============1176586998==\") NIL NIL)\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + BodyPartMultipart multipart; + BodyPartBasic msword; + BodyPartText plain; + BodyPart body; + + engine.SetStream (tokenizer); + + try { + body = await ImapUtils.ParseBodyAsync (engine, "Unexpected token: {0}", string.Empty, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing BODYSTRUCTURE failed: {ex}"); + return; + } + + var token = await engine.ReadTokenAsync (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (body, Is.InstanceOf (), "Body types did not match."); + multipart = (BodyPartMultipart) body; + + Assert.That (body.ContentType.IsMimeType ("multipart", "mixed"), Is.True, "Content-Type did not match."); + Assert.That (body.ContentType.Parameters["boundary"], Is.EqualTo ("===============1176586998=="), "boundary param did not match"); + Assert.That (multipart.BodyParts, Has.Count.EqualTo (3), "BodyParts count does not match."); + + Assert.That (multipart.BodyParts[0], Is.InstanceOf (), "The type of the first child does not match."); + plain = (BodyPartText) multipart.BodyParts[0]; + Assert.That (plain.ContentType.MimeType, Is.EqualTo ("text/plain"), "Content-Type did not match."); + Assert.That (plain.ContentType.Charset, Is.EqualTo ("iso-8859-1"), "Content-Type charset parameter did not match."); + Assert.That (plain.ContentTransferEncoding, Is.EqualTo ("quoted-printable"), "Content-Transfer-Encoding did not match."); + Assert.That (plain.ContentDescription, Is.EqualTo ("Mail message body"), "Content-Description did not match."); + Assert.That (plain.Octets, Is.EqualTo (2201), "Octets did not match."); + Assert.That (plain.Lines, Is.EqualTo (34), "Lines did not match."); + Assert.That (plain.ContentDisposition, Is.Null, "Content-Disposition did not match."); + + Assert.That (multipart.BodyParts[1], Is.InstanceOf (), "The type of the second child does not match."); + msword = (BodyPartBasic) multipart.BodyParts[1]; + Assert.That (msword.ContentType.MimeType, Is.EqualTo ("application/msword"), "Content-Type did not match."); + Assert.That (msword.ContentTransferEncoding, Is.EqualTo ("base64"), "Content-Transfer-Encoding did not match."); + Assert.That (msword.Octets, Is.EqualTo (50446), "Octets did not match."); + Assert.That (msword.ContentDisposition, Is.Null, "Content-Disposition did not match."); + + Assert.That (multipart.BodyParts[2], Is.InstanceOf (), "The type of the second child does not match."); + msword = (BodyPartBasic) multipart.BodyParts[2]; + Assert.That (msword.ContentType.MimeType, Is.EqualTo ("application/msword"), "Content-Type did not match."); + Assert.That (msword.ContentTransferEncoding, Is.EqualTo ("base64"), "Content-Transfer-Encoding did not match."); + Assert.That (msword.Octets, Is.EqualTo (45544), "Octets did not match."); + Assert.That (msword.ContentDisposition.Disposition, Is.EqualTo ("attachment"), "Content-Disposition did not match."); + Assert.That (msword.ContentDisposition.FileName, Is.EqualTo ("PREIS ANSPRUCHS FORMULAR.doc"), "Filename parameters do not match."); + } + } + } + } + + [Test] + public void TestParseBodyStructureWithSwappedBodyFldDspAndBodyFldLang () + { + const string text = "(((\"text\" \"plain\" (\"format\" \"flowed\" \"charset\" \"UTF-8\") NIL NIL \"8bit\" 314 8 NIL NIL NIL NIL)(\"text\" \"html\" (\"charset\" \"UTF-8\") NIL NIL \"8bit\" 763 18 NIL NIL NIL NIL) \"alternative\" (\"boundary\" \"b3_f0dcbd2fdb06033cba91309b09af1cd8\") NIL NIL NIL NIL)(\"image\" \"jpeg\" (\"name\" \"18e5ca259ceb18af6dd3ea0659f83a4c\") \"<18e5ca259ceb18af6dd3ea0659f83a4c>\" NIL \"base64\" 334384 NIL NIL NIL NIL)(\"image\" \"png\" (\"name\" \"87c487a1ff757e32ee27ff267d28af35\") \"<87c487a1ff757e32ee27ff267d28af35>\" NIL \"base64\" 375634 NIL NIL NIL NIL) \"related\" (\"type\" \"multipart/alternative\" \"charset\" \"UTF-8\" \"boundary\" \"b1_f0dcbd2fdb06033cba91309b09af1cd8\") NIL (\"inline\" NIL) NIL NIL)\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + BodyPartMultipart multipart; + BodyPart body; + + engine.SetStream (tokenizer); + + try { + body = ImapUtils.ParseBody (engine, "Unexpected token: {0}", string.Empty, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing BODYSTRUCTURE failed: {ex}"); + return; + } + + var token = engine.ReadToken (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (body, Is.InstanceOf (), "Body types did not match."); + multipart = (BodyPartMultipart) body; + + Assert.That (multipart.ContentType.IsMimeType ("multipart", "related"), Is.True, "Content-Type did not match."); + Assert.That (multipart.ContentType.Parameters["boundary"], Is.EqualTo ("b1_f0dcbd2fdb06033cba91309b09af1cd8"), "boundary param did not match"); + Assert.That (multipart.BodyParts, Has.Count.EqualTo (3), "BodyParts count does not match."); + Assert.That (multipart.ContentLanguage, Has.Length.EqualTo (1), "Content-Language lengths do not match."); + Assert.That (multipart.ContentLanguage[0], Is.EqualTo ("inline"), "Content-Language does not match."); + } + } + } + } + + [Test] + public async Task TestParseBodyStructureWithSwappedBodyFldDspAndBodyFldLangAsync () + { + const string text = "(((\"text\" \"plain\" (\"format\" \"flowed\" \"charset\" \"UTF-8\") NIL NIL \"8bit\" 314 8 NIL NIL NIL NIL)(\"text\" \"html\" (\"charset\" \"UTF-8\") NIL NIL \"8bit\" 763 18 NIL NIL NIL NIL) \"alternative\" (\"boundary\" \"b3_f0dcbd2fdb06033cba91309b09af1cd8\") NIL NIL NIL NIL)(\"image\" \"jpeg\" (\"name\" \"18e5ca259ceb18af6dd3ea0659f83a4c\") \"<18e5ca259ceb18af6dd3ea0659f83a4c>\" NIL \"base64\" 334384 NIL NIL NIL NIL)(\"image\" \"png\" (\"name\" \"87c487a1ff757e32ee27ff267d28af35\") \"<87c487a1ff757e32ee27ff267d28af35>\" NIL \"base64\" 375634 NIL NIL NIL NIL) \"related\" (\"type\" \"multipart/alternative\" \"charset\" \"UTF-8\" \"boundary\" \"b1_f0dcbd2fdb06033cba91309b09af1cd8\") NIL (\"inline\" NIL) NIL NIL)\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + BodyPartMultipart multipart; + BodyPart body; + + engine.SetStream (tokenizer); + + try { + body = await ImapUtils.ParseBodyAsync (engine, "Unexpected token: {0}", string.Empty, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing BODYSTRUCTURE failed: {ex}"); + return; + } + + var token = await engine.ReadTokenAsync (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (body, Is.InstanceOf (), "Body types did not match."); + multipart = (BodyPartMultipart) body; + + Assert.That (multipart.ContentType.IsMimeType ("multipart", "related"), Is.True, "Content-Type did not match."); + Assert.That (multipart.ContentType.Parameters["boundary"], Is.EqualTo ("b1_f0dcbd2fdb06033cba91309b09af1cd8"), "boundary param did not match"); + Assert.That (multipart.BodyParts, Has.Count.EqualTo (3), "BodyParts count does not match."); + Assert.That (multipart.ContentLanguage, Has.Length.EqualTo (1), "Content-Language lengths do not match."); + Assert.That (multipart.ContentLanguage[0], Is.EqualTo ("inline"), "Content-Language does not match."); + } + } + } + } + + // This tests a work-around for a bug in Exchange that was reported via email. + [Test] + public void TestParseBodyStructureWithNegativeOctetValue () + { + const string text = "(\"multipart\" \"digest\" (\"boundary\" \"ommgDs4vJ6fX2nQAghXj4aUy9wsHMMDb\") NIL NIL \"7BIT\" -1 NIL NIL NIL NIL)\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + BodyPartBasic basic; + BodyPart body; + + engine.SetStream (tokenizer); + + try { + body = ImapUtils.ParseBody (engine, "Unexpected token: {0}", string.Empty, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing BODYSTRUCTURE failed: {ex}"); + return; + } + + var token = engine.ReadToken (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (body, Is.InstanceOf (), "Body types did not match."); + basic = (BodyPartBasic) body; + + Assert.That (basic.ContentType.IsMimeType ("multipart", "digest"), Is.True, "Content-Type did not match."); + Assert.That (basic.ContentType.Parameters["boundary"], Is.EqualTo ("ommgDs4vJ6fX2nQAghXj4aUy9wsHMMDb"), "boundary param did not match"); + Assert.That (basic.ContentTransferEncoding, Is.EqualTo ("7BIT"), "Content-Transfer-Encoding did not match."); + Assert.That (basic.Octets, Is.EqualTo (0), "Octets did not match."); + Assert.That (basic.ContentDisposition, Is.Null, "Content-Disposition did not match."); + } + } + } + } + + // This tests a work-around for a bug in Exchange that was reported via email. + [Test] + public async Task TestParseBodyStructureWithNegativeOctetValueAsync () + { + const string text = "(\"multipart\" \"digest\" (\"boundary\" \"ommgDs4vJ6fX2nQAghXj4aUy9wsHMMDb\") NIL NIL \"7BIT\" -1 NIL NIL NIL NIL)\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + BodyPartBasic basic; + BodyPart body; + + engine.SetStream (tokenizer); + + try { + body = await ImapUtils.ParseBodyAsync (engine, "Unexpected token: {0}", string.Empty, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing BODYSTRUCTURE failed: {ex}"); + return; + } + + var token = await engine.ReadTokenAsync (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (body, Is.InstanceOf (), "Body types did not match."); + basic = (BodyPartBasic) body; + + Assert.That (basic.ContentType.IsMimeType ("multipart", "digest"), Is.True, "Content-Type did not match."); + Assert.That (basic.ContentType.Parameters["boundary"], Is.EqualTo ("ommgDs4vJ6fX2nQAghXj4aUy9wsHMMDb"), "boundary param did not match"); + Assert.That (basic.ContentTransferEncoding, Is.EqualTo ("7BIT"), "Content-Transfer-Encoding did not match."); + Assert.That (basic.Octets, Is.EqualTo (0), "Octets did not match."); + Assert.That (basic.ContentDisposition, Is.Null, "Content-Disposition did not match."); + } + } + } + } + + [Test] + public void TestParseBodyStructureWithNilMultipartBody () + { + const string text = "((\"text\" \"plain\" (\"charset\" \"utf-8\") NIL NIL \"7bit\" 727 16 NIL NIL NIL NIL)(\"message\" \"delivery-status\" (\"name\" \"Delivery status\") NIL NIL \"7bit\" 416 NIL NIL NIL NIL)(\"message\" \"rfc822\" (\"name\" \"Message headers\") NIL NIL \"7bit\" 903 (\"Mon, 17 Nov 2014 13:29:21 +0100\" \"Re: Adresy\" ((\"username\" NIL \"e.username\" \"example.com\")) ((\"username\" NIL \"e.username\" \"example.com\")) ((\"username\" NIL \"e.username\" \"example.com\")) ((\"=?utf-8?Q?Justyna?=\" NIL \"salesde\" \"some-company.eu\")) ((NIL NIL \"saleseu\" \"some-company.eu\")(\"Bogdan\" NIL \"bogdan\" \"some-company.eu\")) NIL \"<004901d00260$35405970$9fc10c50$@some-company.eu>\" \"\") (NIL \"alternative\" (\"boundary\" \"Apple-Mail=_352FCEEC-EB15-428F-9D8B-D3B4259DD646\") NIL NIL NIL) 17 NIL NIL NIL NIL) \"report\" (\"report-type\" \"delivery-status\" \"boundary\" \"_e0d7475d888f9882b71de053e5efb221_idea\") NIL NIL NIL)\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + BodyPart body; + + engine.SetStream (tokenizer); + + try { + body = ImapUtils.ParseBody (engine, "Syntax error in BODYSTRUCTURE: {0}", string.Empty, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing BODYSTRUCTURE failed: {ex}"); + return; + } + + var token = engine.ReadToken (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (body, Is.InstanceOf (), "Body types did not match."); + var multipart = (BodyPartMultipart) body; + + Assert.That (multipart.ContentType.IsMimeType ("multipart", "report"), Is.True, "Content-Type did not match."); + Assert.That (multipart.ContentType.Parameters["report-type"], Is.EqualTo ("delivery-status"), "report-type param did not match"); + Assert.That (multipart.ContentType.Boundary, Is.EqualTo ("_e0d7475d888f9882b71de053e5efb221_idea"), "boundary param did not match"); + Assert.That (multipart.BodyParts, Has.Count.EqualTo (3), "multipart children did not match"); + + Assert.That (multipart.BodyParts[0], Is.InstanceOf (), "First multipart subpart types did not match."); + var plain = (BodyPartText) multipart.BodyParts[0]; + Assert.That (plain.ContentTransferEncoding, Is.EqualTo ("7bit"), "Content-Transfer-Encoding did not match."); + Assert.That (plain.Octets, Is.EqualTo (727), "Octets did not match."); + Assert.That (plain.Lines, Is.EqualTo (16), "Lines did not match."); + + Assert.That (multipart.BodyParts[1], Is.InstanceOf (), "Second multipart subpart types did not match."); + var deliveryStatus = (BodyPartBasic) multipart.BodyParts[1]; + Assert.That (deliveryStatus.ContentType.Name, Is.EqualTo ("Delivery status"), "name param did not match"); + Assert.That (deliveryStatus.ContentTransferEncoding, Is.EqualTo ("7bit"), "Content-Transfer-Encoding did not match."); + Assert.That (deliveryStatus.Octets, Is.EqualTo (416), "Octets did not match."); + + Assert.That (multipart.BodyParts[2], Is.InstanceOf (), "Third multipart subpart types did not match."); + var rfc822 = (BodyPartMessage) multipart.BodyParts[2]; + Assert.That (rfc822.ContentType.Name, Is.EqualTo ("Message headers"), "name param did not match"); + Assert.That (rfc822.ContentTransferEncoding, Is.EqualTo ("7bit"), "Content-Transfer-Encoding did not match."); + Assert.That (rfc822.Octets, Is.EqualTo (903), "Octets did not match."); + Assert.That (rfc822.Lines, Is.EqualTo (17), "Lines did not match."); + + Assert.That (rfc822.Body, Is.InstanceOf (), "rfc822 body types did not match."); + var alternative = (BodyPartMultipart) rfc822.Body; + Assert.That (alternative.ContentType.IsMimeType ("multipart", "alternative"), Is.True, "Content-Type did not match."); + Assert.That (alternative.ContentType.Boundary, Is.EqualTo ("Apple-Mail=_352FCEEC-EB15-428F-9D8B-D3B4259DD646"), "boundary param did not match"); + Assert.That (alternative.BodyParts, Is.Empty, "alternative bodyparts count did not match."); + } + } + } + } + + [Test] + public async Task TestParseBodyStructureWithNilMultipartBodyAsync () + { + const string text = "((\"text\" \"plain\" (\"charset\" \"utf-8\") NIL NIL \"7bit\" 727 16 NIL NIL NIL NIL)(\"message\" \"delivery-status\" (\"name\" \"Delivery status\") NIL NIL \"7bit\" 416 NIL NIL NIL NIL)(\"message\" \"rfc822\" (\"name\" \"Message headers\") NIL NIL \"7bit\" 903 (\"Mon, 17 Nov 2014 13:29:21 +0100\" \"Re: Adresy\" ((\"username\" NIL \"e.username\" \"example.com\")) ((\"username\" NIL \"e.username\" \"example.com\")) ((\"username\" NIL \"e.username\" \"example.com\")) ((\"=?utf-8?Q?Justyna?=\" NIL \"salesde\" \"some-company.eu\")) ((NIL NIL \"saleseu\" \"some-company.eu\")(\"Bogdan\" NIL \"bogdan\" \"some-company.eu\")) NIL \"<004901d00260$35405970$9fc10c50$@some-company.eu>\" \"\") (NIL \"alternative\" (\"boundary\" \"Apple-Mail=_352FCEEC-EB15-428F-9D8B-D3B4259DD646\") NIL NIL NIL) 17 NIL NIL NIL NIL) \"report\" (\"report-type\" \"delivery-status\" \"boundary\" \"_e0d7475d888f9882b71de053e5efb221_idea\") NIL NIL NIL)\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + BodyPart body; + + engine.SetStream (tokenizer); + + try { + body = await ImapUtils.ParseBodyAsync (engine, "Syntax error in BODYSTRUCTURE: {0}", string.Empty, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing BODYSTRUCTURE failed: {ex}"); + return; + } + + var token = engine.ReadToken (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (body, Is.InstanceOf (), "Body types did not match."); + var multipart = (BodyPartMultipart) body; + + Assert.That (multipart.ContentType.IsMimeType ("multipart", "report"), Is.True, "Content-Type did not match."); + Assert.That (multipart.ContentType.Parameters["report-type"], Is.EqualTo ("delivery-status"), "report-type param did not match"); + Assert.That (multipart.ContentType.Boundary, Is.EqualTo ("_e0d7475d888f9882b71de053e5efb221_idea"), "boundary param did not match"); + Assert.That (multipart.BodyParts, Has.Count.EqualTo (3), "multipart children did not match"); + + Assert.That (multipart.BodyParts[0], Is.InstanceOf (), "First multipart subpart types did not match."); + var plain = (BodyPartText) multipart.BodyParts[0]; + Assert.That (plain.ContentTransferEncoding, Is.EqualTo ("7bit"), "Content-Transfer-Encoding did not match."); + Assert.That (plain.Octets, Is.EqualTo (727), "Octets did not match."); + Assert.That (plain.Lines, Is.EqualTo (16), "Lines did not match."); + + Assert.That (multipart.BodyParts[1], Is.InstanceOf (), "Second multipart subpart types did not match."); + var deliveryStatus = (BodyPartBasic) multipart.BodyParts[1]; + Assert.That (deliveryStatus.ContentType.Name, Is.EqualTo ("Delivery status"), "name param did not match"); + Assert.That (deliveryStatus.ContentTransferEncoding, Is.EqualTo ("7bit"), "Content-Transfer-Encoding did not match."); + Assert.That (deliveryStatus.Octets, Is.EqualTo (416), "Octets did not match."); + + Assert.That (multipart.BodyParts[2], Is.InstanceOf (), "Third multipart subpart types did not match."); + var rfc822 = (BodyPartMessage) multipart.BodyParts[2]; + Assert.That (rfc822.ContentType.Name, Is.EqualTo ("Message headers"), "name param did not match"); + Assert.That (rfc822.ContentTransferEncoding, Is.EqualTo ("7bit"), "Content-Transfer-Encoding did not match."); + Assert.That (rfc822.Octets, Is.EqualTo (903), "Octets did not match."); + Assert.That (rfc822.Lines, Is.EqualTo (17), "Lines did not match."); + + Assert.That (rfc822.Body, Is.InstanceOf (), "rfc822 body types did not match."); + var alternative = (BodyPartMultipart) rfc822.Body; + Assert.That (alternative.ContentType.IsMimeType ("multipart", "alternative"), Is.True, "Content-Type did not match."); + Assert.That (alternative.ContentType.Boundary, Is.EqualTo ("Apple-Mail=_352FCEEC-EB15-428F-9D8B-D3B4259DD646"), "boundary param did not match"); + Assert.That (alternative.BodyParts, Is.Empty, "alternative bodyparts count did not match."); + } + } + } + } + + static void AssertParseBadlyFormedBodyStructureWithCompletelyNilBodyParts1 (BodyPart body) + { + Assert.That (body, Is.InstanceOf (), "Body types did not match."); + var multipart = (BodyPartMultipart) body; + + Assert.That (multipart.ContentType.IsMimeType ("multipart", "mixed"), Is.True, "Content-Type did not match."); + Assert.That (multipart.ContentType.Boundary, Is.EqualTo ("008_BN0P221MB04483769DDD81948BC7C387DC8889BN0P221MB0448NAMP"), "boundary param did not match"); + Assert.That (multipart.BodyParts, Has.Count.EqualTo (2), "multipart children did not match"); + + Assert.That (multipart.BodyParts[0], Is.InstanceOf (), "First multipart/mixed subpart types did not match."); + var related = (BodyPartMultipart) multipart.BodyParts[0]; + Assert.That (related.ContentType.IsMimeType ("multipart", "related"), Is.True, "Content-Type did not match."); + Assert.That (related.ContentType.Parameters["type"], Is.EqualTo ("multipart/alternative"), "type param did not match"); + Assert.That (related.ContentType.Boundary, Is.EqualTo ("007_BN0P221MB04483769DDD81948BC7C387DC8889BN0P221MB0448NAMP"), "boundary param did not match"); + Assert.That (related.BodyParts, Has.Count.EqualTo (4), "multipart children did not match"); + + Assert.That (related.BodyParts[0], Is.InstanceOf (), "First multipart/related subpart types did not match."); + var alternative = (BodyPartMultipart) related.BodyParts[0]; + Assert.That (alternative.ContentType.IsMimeType ("multipart", "alternative"), Is.True, "Content-Type did not match."); + Assert.That (alternative.ContentType.Boundary, Is.EqualTo ("000_BN0P221MB04483769DDD81948BC7C387DC8889BN0P221MB0448NAMP"), "boundary param did not match"); + Assert.That (alternative.BodyParts, Has.Count.EqualTo (2), "multipart children did not match"); + + Assert.That (alternative.BodyParts[0], Is.InstanceOf (), "First multipart/alternative subpart types did not match."); + var plain = (BodyPartText) alternative.BodyParts[0]; + Assert.That (plain.ContentType.IsMimeType ("text", "plain"), Is.True, "Content-Type did not match."); + Assert.That (plain.ContentType.Charset, Is.EqualTo ("us-ascii"), "Charset parameter did not match"); + Assert.That (plain.ContentTransferEncoding, Is.EqualTo ("quoted-printable"), "Content-Transfer-Encoding did not match."); + Assert.That (plain.Octets, Is.EqualTo (44619), "Octets did not match."); + Assert.That (plain.Lines, Is.EqualTo (793), "Lines did not match."); + + Assert.That (alternative.BodyParts[1], Is.InstanceOf (), "Second multipart/alternative subpart types did not match."); + var html = (BodyPartText) alternative.BodyParts[1]; + Assert.That (html.ContentType.IsMimeType ("text", "html"), Is.True, "Content-Type did not match."); + Assert.That (html.ContentTransferEncoding, Is.EqualTo ("quoted-printable"), "Content-Transfer-Encoding did not match."); + Assert.That (html.Octets, Is.EqualTo (143984), "Octets did not match."); + Assert.That (html.Lines, Is.EqualTo (2321), "Lines did not match."); + + Assert.That (related.BodyParts[1], Is.InstanceOf (), "Second multipart/related subpart types did not match."); + var jpeg = (BodyPartBasic) related.BodyParts[1]; + Assert.That (jpeg.ContentType.IsMimeType ("image", "jpeg"), Is.True, "Content-Type did not match."); + Assert.That (jpeg.ContentType.Name, Is.EqualTo ("~WRD0000.jpg"), "Name parameter did not match"); + Assert.That (jpeg.ContentDisposition.Disposition, Is.EqualTo ("inline"), "Disposition did not match"); + Assert.That (jpeg.ContentDisposition.FileName, Is.EqualTo ("~WRD0000.jpg"), "Filename parameter did not match"); + Assert.That (jpeg.ContentTransferEncoding, Is.EqualTo ("base64"), "Content-Transfer-Encoding did not match."); + Assert.That (jpeg.Octets, Is.EqualTo (1130), "Octets did not match."); + + Assert.That (related.BodyParts[2], Is.InstanceOf (), "Third multipart/related subpart types did not match."); + var png = (BodyPartBasic) related.BodyParts[2]; + Assert.That (png.ContentType.IsMimeType ("image", "png"), Is.True, "Content-Type did not match."); + Assert.That (png.ContentType.Name, Is.EqualTo ("image001.png"), "Name parameter did not match"); + Assert.That (png.ContentDisposition.Disposition, Is.EqualTo ("inline"), "Disposition did not match"); + Assert.That (png.ContentDisposition.FileName, Is.EqualTo ("image001.png"), "Filename parameter did not match"); + Assert.That (png.ContentTransferEncoding, Is.EqualTo ("base64"), "Content-Transfer-Encoding did not match."); + Assert.That (png.Octets, Is.EqualTo (8174), "Octets did not match."); + + Assert.That (related.BodyParts[3], Is.InstanceOf (), "Fourth multipart/related subpart types did not match."); + png = (BodyPartBasic) related.BodyParts[3]; + Assert.That (png.ContentType.IsMimeType ("image", "png"), Is.True, "Content-Type did not match."); + Assert.That (png.ContentType.Name, Is.EqualTo ("image002.png"), "Name parameter did not match"); + Assert.That (png.ContentDisposition.Disposition, Is.EqualTo ("inline"), "Disposition did not match"); + Assert.That (png.ContentDisposition.FileName, Is.EqualTo ("image002.png"), "Filename parameter did not match"); + Assert.That (png.ContentTransferEncoding, Is.EqualTo ("base64"), "Content-Transfer-Encoding did not match."); + Assert.That (png.Octets, Is.EqualTo (3524), "Octets did not match."); + + Assert.That (multipart.BodyParts[1], Is.InstanceOf (), "Second multipart/mixed subpart types did not match."); + var rfc822 = (BodyPartMessage) multipart.BodyParts[1]; + Assert.That (rfc822.ContentType.Name, Is.EqualTo (null), "name param did not match"); + Assert.That (rfc822.ContentTransferEncoding, Is.EqualTo ("7BIT"), "Content-Transfer-Encoding did not match."); + Assert.That (rfc822.Octets, Is.EqualTo (0), "Octets did not match."); + Assert.That (rfc822.Lines, Is.EqualTo (0), "Lines did not match."); + + // Okay, lets skip ahead to the juicy bits... + multipart = (BodyPartMultipart) rfc822.Body; + Assert.That (multipart.ContentType.Boundary, Is.EqualTo ("010_18f52bea798548b88470c3df62d666bcScrubbed"), "boundary param did not match"); + Assert.That (multipart.BodyParts, Has.Count.EqualTo (4), "multipart children did not match"); + + rfc822 = (BodyPartMessage) multipart.BodyParts[2]; + multipart = (BodyPartMultipart) rfc822.Body; + alternative = (BodyPartMultipart) multipart.BodyParts[0]; + + for (int i = 0; i < alternative.BodyParts.Count; i++) { + var nils = (BodyPartBasic) alternative.BodyParts[i]; + + Assert.That (nils.ContentType.IsMimeType ("application", "octet-stream"), Is.True, "Content-Type did not match."); + Assert.That (nils.ContentDescription, Is.Null, "Content-Description should be null"); + Assert.That (nils.ContentDisposition, Is.Null, "Content-Disposition should be null"); + Assert.That (nils.ContentId, Is.Null, "Content-Id should be null"); + Assert.That (nils.ContentLanguage, Is.Null, "Content-Language should be null"); + Assert.That (nils.ContentLocation, Is.Null, "Content-Location should be null"); + Assert.That (nils.ContentMd5, Is.Null, "Content-Md5 should be null"); + Assert.That (nils.ContentTransferEncoding, Is.EqualTo ("7BIT"), "Content-Transfer-Encodings did not match"); + Assert.That (nils.Octets, Is.EqualTo (0), "Octets did not match"); + } + } + + [Test] + public void TestParseBadlyFormedBodyStructureWithCompletelyNilBodyParts1 () + { + const string text = "((((\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"quoted-printable\" 44619 793 NIL NIL NIL NIL)(\"text\" \"html\" (\"charset\" \"us-ascii\") NIL NIL \"quoted-printable\" 143984 2321 NIL NIL NIL NIL) \"alternative\" (\"boundary\" \"000_BN0P221MB04483769DDD81948BC7C387DC8889BN0P221MB0448NAMP\") NIL NIL)(\"image\" \"jpeg\" (\"name\" \"~WRD0000.jpg\") \"<~WRD0000.jpg>\" \"~WRD0000.jpg\" \"base64\" 1130 NIL (\"inline\" (\"filename\" \"~WRD0000.jpg\" \"size\" \"823\" \"creation-date\" \"Thu, 14 Jul 2022 17:26:49 GMT\" \"modification-date\" \"Thu, 14 Jul 2022 17:33:16 GMT\")) NIL NIL)(\"image\" \"png\" (\"name\" \"image001.png\") \"image001.png@01D89786.45095140\" \"image001.png\" \"base64\" 8174 NIL (\"inline\" (\"filename\" \"image001.png\" \"size\" \"5973\" \"creation-date\" \"Thu, 14 Jul 2022 17:33:18 GMT\" \"modification-date\" \"Thu, 14 Jul 2022 17:33:18 GMT\")) NIL NIL)(\"image\" \"png\" (\"name\" \"image002.png\") \"image002.png@01D89786.45095140\" \"image002.png\" \"base64\" 3524 NIL (\"inline\" (\"filename\" \"image002.png\" \"size\" \"2572\" \"creation-date\" \"Thu, 14 Jul 2022 17:33:18 GMT\" \"modification-date\" \"Thu, 14 Jul 2022 17:33:18 GMT\")) NIL NIL) \"related\" (\"boundary\" \"007_BN0P221MB04483769DDD81948BC7C387DC8889BN0P221MB0448NAMP\" \"type\" \"multipart/alternative\") NIL NIL)(\"message\" \"rfc822\" NIL NIL NIL \"7BIT\" 0 (\"Thu, 14 Jul 2022 15:12:33 +0000\" \"Scrubbed\" ((\"Scrubbed\" NIL \"Scrubbed\" \"Scrubbed\")) NIL NIL ((\"Scrubbed\" NIL \"Scrubbed\" \"Scrubbed\")) ((\"Scrubbed\" NIL \"Scrubbed\" \"Scrubbed\") (\"Scrubbed\" NIL \"Scrubbed\" \"Scrubbed\")) NIL \"Scrubbed@Scrubbed.com\" \"Scrubbed@Scrubbed.com\") ((((\"text\" \"plain\" (\"charset\" \"utf-8\") NIL NIL \"base64\" 53608 688 NIL NIL NIL NIL)(\"text\" \"html\" (\"charset\" \"utf-8\") \"Scrubbed@NAMP221.PROD.OUTLOOK.COM\" NIL \"base64\" 176002 2257 NIL NIL NIL NIL) \"alternative\" (\"boundary\" \"000_18f52bea798548b88470c3df62d666bcScrubbed\") NIL NIL)(\"image\" \"png\" (\"name\" \"image001.png\") \"image001.png@01D89770.62F36800\" \"image001.png\" \"base64\" 8174 NIL (\"inline\" (\"filename\" \"image001.png\" \"size\" \"5973\" \"creation-date\" \"Thu, 14 Jul 2022 15:12:32 GMT\" \"modification-date\" \"Thu, 14 Jul 2022 17:33:17 GMT\")) NIL NIL)(\"image\" \"jpeg\" (\"name\" \"image002.jpg\") \"image002.jpg@01D89770.62F36800\" \"image002.jpg\" \"base64\" 1130 NIL (\"inline\" (\"filename\" \"image002.jpg\" \"size\" \"823\" \"creation-date\" \"Thu, 14 Jul 2022 15:12:32 GMT\" \"modification-date\" \"Thu, 14 Jul 2022 17:33:17 GMT\")) NIL NIL)(\"image\" \"png\" (\"name\" \"image003.png\") \"image003.png@01D89770.62F36800\" \"image003.png\" \"base64\" 3524 NIL (\"inline\" (\"filename\" \"image003.png\" \"size\" \"2572\" \"creation-date\" \"Thu, 14 Jul 2022 15:12:32 GMT\" \"modification-date\" \"Thu, 14 Jul 2022 17:33:17 GMT\")) NIL NIL) \"related\" (\"boundary\" \"009_18f52bea798548b88470c3df62d666bcScrubbed\" \"type\" \"multipart/alternative\") NIL NIL)(\"application\" \"pdf\" (\"name\" \"Scrubbed.pdf\") \"Scrubbed@NAMP221.PROD.OUTLOOK.COM\" \"Scrubbed.pdf\" \"base64\" 324012 NIL (\"attachment\" (\"filename\" \"Scrubbed.pdf\" \"size\" \"236776\" \"creation-date\" \"Thu, 14 Jul 2022 14:53:00 GMT\" \"modification-date\" \"Thu, 14 Jul 2022 17:33:17 GMT\")) NIL NIL)(\"message\" \"rfc822\" NIL \"Scrubbed@NAMP221.PROD.OUTLOOK.COM\" NIL \"7BIT\" 0 (\"Tue, 11 Jan 2022 16:34:33 +0000\" \"RE: Scrubbed\" ((\"Scrubbed\" NIL \"Scrubbed\" \"Scrubbed\")) NIL NIL ((\"Scrubbed\" NIL \"Scrubbed\" \"Scrubbed\")) ((\"Scrubbed\" NIL \"Scrubbed\" \"Scrubbed\") (\"Scrubbed\" NIL \"Scrubbed\" \"Scrubbed\") (\"Scrubbed\" NIL \"Scrubbed\" \"Scrubbed\")) NIL \"Scrubbed@Scrubbed.com\" \"Scrubbed@Scrubbed.CANPRD01.PROD.OUTLOOK.COM\") (((NIL NIL NIL NIL NIL \"7BIT\" 0 NIL NIL NIL NIL)(NIL NIL NIL NIL NIL \"7BIT\" 0 NIL NIL NIL NIL)(NIL NIL NIL NIL NIL \"7BIT\" 0 NIL NIL NIL NIL)(NIL NIL NIL NIL NIL \"7BIT\" 0 NIL NIL NIL NIL) \"related\" (\"boundary\" \"007_YT2PR01MB47524CF92A3AD1F75AFF2D25D9519YT2PR01MB4752CANP\" \"type\" \"multipart/alternative\") NIL NIL)(\"application\" \"pdf\" (\"name\" \"Scrubbed.pdf\") NIL \"Scrubbed.pdf\" \"base64\" 215638 NIL (\"attachment\" (\"filename\" \"Scrubbed.pdf\" \"size\" \"157579\" \"creation-date\" \"Wed, 02 Feb 2022 21:33:39 GMT\" \"modification-date\" \"Wed, 02 Feb 2022 21:33:39 GMT\")) NIL NIL) \"mixed\" (\"boundary\" \"008_YT2PR01MB47524CF92A3AD1F75AFF2D25D9519YT2PR01MB4752CANP\") NIL \"en-US\") 0 NIL (\"attachment\" (\"creation-date\" \"Thu, 14 Jul 2022 15:12:31 GMT\" \"modification-date\" \"Thu, 14 Jul 2022 17:33:18 GMT\")) NIL NIL)(\"application\" \"pdf\" (\"name\" \"Scrubbed.pdf?=\") \"Scrubbed@NAMP221.PROD.OUTLOOK.COM\" \"Scrubbed.pdf?=\" \"base64\" 208376 NIL (\"attachment\" (\"filename\" \"Scrubbed.pdf?=\" \"size\" \"152274\" \"creation-date\" \"Thu, 14 Jul 2022 15:05:00 GMT\" \"modification-date\" \"Thu, 14 Jul 2022 17:33:18 GMT\")) NIL NIL) \"mixed\" (\"boundary\" \"010_18f52bea798548b88470c3df62d666bcScrubbed\") NIL \"en-US\") 0 NIL (\"attachment\" (\"creation-date\" \"Thu, 14 Jul 2022 17:33:16 GMT\" \"modification-date\" \"Thu, 14 Jul 2022 17:33:18 GMT\")) NIL NIL) \"mixed\" (\"boundary\" \"008_BN0P221MB04483769DDD81948BC7C387DC8889BN0P221MB0448NAMP\") NIL \"en-US\")\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + BodyPart body; + + engine.SetStream (tokenizer); + + try { + body = ImapUtils.ParseBody (engine, "Syntax error in BODYSTRUCTURE: {0}", string.Empty, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing BODYSTRUCTURE failed: {ex}"); + return; + } + + var token = engine.ReadToken (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + AssertParseBadlyFormedBodyStructureWithCompletelyNilBodyParts1 (body); + } + } + } + } + + [Test] + public async Task TestParseBadlyFormedBodyStructureWithCompletelyNilBodyParts1Async () + { + const string text = "((((\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"quoted-printable\" 44619 793 NIL NIL NIL NIL)(\"text\" \"html\" (\"charset\" \"us-ascii\") NIL NIL \"quoted-printable\" 143984 2321 NIL NIL NIL NIL) \"alternative\" (\"boundary\" \"000_BN0P221MB04483769DDD81948BC7C387DC8889BN0P221MB0448NAMP\") NIL NIL)(\"image\" \"jpeg\" (\"name\" \"~WRD0000.jpg\") \"<~WRD0000.jpg>\" \"~WRD0000.jpg\" \"base64\" 1130 NIL (\"inline\" (\"filename\" \"~WRD0000.jpg\" \"size\" \"823\" \"creation-date\" \"Thu, 14 Jul 2022 17:26:49 GMT\" \"modification-date\" \"Thu, 14 Jul 2022 17:33:16 GMT\")) NIL NIL)(\"image\" \"png\" (\"name\" \"image001.png\") \"image001.png@01D89786.45095140\" \"image001.png\" \"base64\" 8174 NIL (\"inline\" (\"filename\" \"image001.png\" \"size\" \"5973\" \"creation-date\" \"Thu, 14 Jul 2022 17:33:18 GMT\" \"modification-date\" \"Thu, 14 Jul 2022 17:33:18 GMT\")) NIL NIL)(\"image\" \"png\" (\"name\" \"image002.png\") \"image002.png@01D89786.45095140\" \"image002.png\" \"base64\" 3524 NIL (\"inline\" (\"filename\" \"image002.png\" \"size\" \"2572\" \"creation-date\" \"Thu, 14 Jul 2022 17:33:18 GMT\" \"modification-date\" \"Thu, 14 Jul 2022 17:33:18 GMT\")) NIL NIL) \"related\" (\"boundary\" \"007_BN0P221MB04483769DDD81948BC7C387DC8889BN0P221MB0448NAMP\" \"type\" \"multipart/alternative\") NIL NIL)(\"message\" \"rfc822\" NIL NIL NIL \"7BIT\" 0 (\"Thu, 14 Jul 2022 15:12:33 +0000\" \"Scrubbed\" ((\"Scrubbed\" NIL \"Scrubbed\" \"Scrubbed\")) NIL NIL ((\"Scrubbed\" NIL \"Scrubbed\" \"Scrubbed\")) ((\"Scrubbed\" NIL \"Scrubbed\" \"Scrubbed\") (\"Scrubbed\" NIL \"Scrubbed\" \"Scrubbed\")) NIL \"Scrubbed@Scrubbed.com\" \"Scrubbed@Scrubbed.com\") ((((\"text\" \"plain\" (\"charset\" \"utf-8\") NIL NIL \"base64\" 53608 688 NIL NIL NIL NIL)(\"text\" \"html\" (\"charset\" \"utf-8\") \"Scrubbed@NAMP221.PROD.OUTLOOK.COM\" NIL \"base64\" 176002 2257 NIL NIL NIL NIL) \"alternative\" (\"boundary\" \"000_18f52bea798548b88470c3df62d666bcScrubbed\") NIL NIL)(\"image\" \"png\" (\"name\" \"image001.png\") \"image001.png@01D89770.62F36800\" \"image001.png\" \"base64\" 8174 NIL (\"inline\" (\"filename\" \"image001.png\" \"size\" \"5973\" \"creation-date\" \"Thu, 14 Jul 2022 15:12:32 GMT\" \"modification-date\" \"Thu, 14 Jul 2022 17:33:17 GMT\")) NIL NIL)(\"image\" \"jpeg\" (\"name\" \"image002.jpg\") \"image002.jpg@01D89770.62F36800\" \"image002.jpg\" \"base64\" 1130 NIL (\"inline\" (\"filename\" \"image002.jpg\" \"size\" \"823\" \"creation-date\" \"Thu, 14 Jul 2022 15:12:32 GMT\" \"modification-date\" \"Thu, 14 Jul 2022 17:33:17 GMT\")) NIL NIL)(\"image\" \"png\" (\"name\" \"image003.png\") \"image003.png@01D89770.62F36800\" \"image003.png\" \"base64\" 3524 NIL (\"inline\" (\"filename\" \"image003.png\" \"size\" \"2572\" \"creation-date\" \"Thu, 14 Jul 2022 15:12:32 GMT\" \"modification-date\" \"Thu, 14 Jul 2022 17:33:17 GMT\")) NIL NIL) \"related\" (\"boundary\" \"009_18f52bea798548b88470c3df62d666bcScrubbed\" \"type\" \"multipart/alternative\") NIL NIL)(\"application\" \"pdf\" (\"name\" \"Scrubbed.pdf\") \"Scrubbed@NAMP221.PROD.OUTLOOK.COM\" \"Scrubbed.pdf\" \"base64\" 324012 NIL (\"attachment\" (\"filename\" \"Scrubbed.pdf\" \"size\" \"236776\" \"creation-date\" \"Thu, 14 Jul 2022 14:53:00 GMT\" \"modification-date\" \"Thu, 14 Jul 2022 17:33:17 GMT\")) NIL NIL)(\"message\" \"rfc822\" NIL \"Scrubbed@NAMP221.PROD.OUTLOOK.COM\" NIL \"7BIT\" 0 (\"Tue, 11 Jan 2022 16:34:33 +0000\" \"RE: Scrubbed\" ((\"Scrubbed\" NIL \"Scrubbed\" \"Scrubbed\")) NIL NIL ((\"Scrubbed\" NIL \"Scrubbed\" \"Scrubbed\")) ((\"Scrubbed\" NIL \"Scrubbed\" \"Scrubbed\") (\"Scrubbed\" NIL \"Scrubbed\" \"Scrubbed\") (\"Scrubbed\" NIL \"Scrubbed\" \"Scrubbed\")) NIL \"Scrubbed@Scrubbed.com\" \"Scrubbed@Scrubbed.CANPRD01.PROD.OUTLOOK.COM\") (((NIL NIL NIL NIL NIL \"7BIT\" 0 NIL NIL NIL NIL)(NIL NIL NIL NIL NIL \"7BIT\" 0 NIL NIL NIL NIL)(NIL NIL NIL NIL NIL \"7BIT\" 0 NIL NIL NIL NIL)(NIL NIL NIL NIL NIL \"7BIT\" 0 NIL NIL NIL NIL) \"related\" (\"boundary\" \"007_YT2PR01MB47524CF92A3AD1F75AFF2D25D9519YT2PR01MB4752CANP\" \"type\" \"multipart/alternative\") NIL NIL)(\"application\" \"pdf\" (\"name\" \"Scrubbed.pdf\") NIL \"Scrubbed.pdf\" \"base64\" 215638 NIL (\"attachment\" (\"filename\" \"Scrubbed.pdf\" \"size\" \"157579\" \"creation-date\" \"Wed, 02 Feb 2022 21:33:39 GMT\" \"modification-date\" \"Wed, 02 Feb 2022 21:33:39 GMT\")) NIL NIL) \"mixed\" (\"boundary\" \"008_YT2PR01MB47524CF92A3AD1F75AFF2D25D9519YT2PR01MB4752CANP\") NIL \"en-US\") 0 NIL (\"attachment\" (\"creation-date\" \"Thu, 14 Jul 2022 15:12:31 GMT\" \"modification-date\" \"Thu, 14 Jul 2022 17:33:18 GMT\")) NIL NIL)(\"application\" \"pdf\" (\"name\" \"Scrubbed.pdf?=\") \"Scrubbed@NAMP221.PROD.OUTLOOK.COM\" \"Scrubbed.pdf?=\" \"base64\" 208376 NIL (\"attachment\" (\"filename\" \"Scrubbed.pdf?=\" \"size\" \"152274\" \"creation-date\" \"Thu, 14 Jul 2022 15:05:00 GMT\" \"modification-date\" \"Thu, 14 Jul 2022 17:33:18 GMT\")) NIL NIL) \"mixed\" (\"boundary\" \"010_18f52bea798548b88470c3df62d666bcScrubbed\") NIL \"en-US\") 0 NIL (\"attachment\" (\"creation-date\" \"Thu, 14 Jul 2022 17:33:16 GMT\" \"modification-date\" \"Thu, 14 Jul 2022 17:33:18 GMT\")) NIL NIL) \"mixed\" (\"boundary\" \"008_BN0P221MB04483769DDD81948BC7C387DC8889BN0P221MB0448NAMP\") NIL \"en-US\")\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + BodyPart body; + + engine.SetStream (tokenizer); + + try { + body = await ImapUtils.ParseBodyAsync (engine, "Syntax error in BODYSTRUCTURE: {0}", string.Empty, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing BODYSTRUCTURE failed: {ex}"); + return; + } + + var token = await engine.ReadTokenAsync (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + AssertParseBadlyFormedBodyStructureWithCompletelyNilBodyParts1 (body); + } + } + } + } + + static void AssertParseBadlyFormedBodyStructureWithCompletelyNilBodyParts2 (BodyPart body) + { + Assert.That (body, Is.InstanceOf (), "Body types did not match."); + var multipart = (BodyPartMultipart) body; + + Assert.That (multipart.ContentType.IsMimeType ("multipart", "report"), Is.True, "Content-Type did not match."); + Assert.That (multipart.ContentType.Boundary, Is.EqualTo ("272F16D4031920.1659452466/hermes.gatewaynet.com"), "boundary param did not match"); + Assert.That (multipart.BodyParts, Has.Count.EqualTo (3), "multipart children did not match"); + + Assert.That (multipart.BodyParts[0], Is.InstanceOf (), "First multipart/report subpart types did not match."); + var nils = (BodyPartBasic) multipart.BodyParts[0]; + Assert.That (nils.ContentType.IsMimeType ("application", "octet-stream"), Is.True, "Content-Type did not match."); + Assert.That (nils.ContentDescription, Is.Null, "Content-Description should be null"); + Assert.That (nils.ContentDisposition, Is.Null, "Content-Disposition should be null"); + Assert.That (nils.ContentId, Is.Null, "Content-Id should be null"); + Assert.That (nils.ContentLanguage, Is.Null, "Content-Language should be null"); + Assert.That (nils.ContentLocation, Is.Null, "Content-Location should be null"); + Assert.That (nils.ContentMd5, Is.Null, "Content-Md5 should be null"); + Assert.That (nils.ContentTransferEncoding, Is.EqualTo ("7BIT"), "Content-Transfer-Encodings did not match"); + Assert.That (nils.Octets, Is.EqualTo (563), "Octets did not match"); + } + + [Test] + public void TestParseBadlyFormedBodyStructureWithCompletelyNilBodyParts2 () + { + const string text = "((NIL NIL NIL NIL NIL \"7BIT\" 563 NIL NIL NIL NIL)(\"message\" \"delivery-status\" NIL NIL NIL \"7BIT\" 658 NIL NIL NIL NIL)(\"message\" \"rfc822\" NIL NIL NIL \"8bit\" 0 (\"Tue, 2 Aug 2022 15:00:47 +0000\" \"[POSSIBLE SPAM 11.4] Invoices now overdue - 115365#\" ((NIL NIL \"MAILBOX\" \"OUR-DOMAIN\")) NIL NIL ((NIL NIL \"accounts\" \"OTHER-DOMAIN\") (NIL NIL \"safety\" \"OTHER-DOMAIN\") (NIL NIL \"USER\" \"OUR-DOMAIN\")) NIL NIL NIL \"<1IOGPFNLIHU4.377MHPZYJQ6E3@OUR-SERVER>\") (((\"text\" \"plain\" (\"charset\" \"utf-8\") NIL NIL \"8bit\" 597 16 NIL NIL NIL NIL)((\"text\" \"html\" (\"charset\" \"utf-8\") NIL NIL \"7BIT\" 1611 26 NIL NIL NIL NIL)(\"image\" \"png\" (\"name\" \"0.dat\") \"<1KWGPFNLIHU4.4RR7HCVM8MQQ1@OUR-SERVER>\" NIL \"base64\" 14172 NIL (\"inline\" (\"filename\" \"0.dat\")) NIL \"0.dat\")(\"image\" \"png\" (\"name\" \"1.dat\") \"<1KWGPFNLIHU4.UWJ8R86RE2KA2@OUR-SERVER>\" NIL \"base64\" 486 NIL (\"inline\" (\"filename\" \"1.dat\")) NIL \"1.dat\")(\"image\" \"png\" (\"name\" \"2.dat\") \"<1KWGPFNLIHU4.EC7HN124OJC32@OUR-SERVER>\" NIL \"base64\" 506 NIL (\"inline\" (\"filename\" \"2.dat\")) NIL \"2.dat\")(\"image\" \"png\" (\"name\" \"3.dat\") \"<1KWGPFNLIHU4.WM1ALJTG745F1@OUR-SERVER>\" NIL \"base64\" 616 NIL (\"inline\" (\"filename\" \"3.dat\")) NIL \"3.dat\")(\"image\" \"png\" (\"name\" \"4.dat\") \"<1KWGPFNLIHU4.1B42S5EVSF4B2@OUR-SERVER>\" NIL \"base64\" 22470 NIL (\"inline\" (\"filename\" \"4.dat\")) NIL \"4.dat\") \"related\" (\"boundary\" \"=-5nEE2FIlRoeXkJyZAHV8UA==\" \"type\" \"text/html\") NIL NIL) \"alternative\" (\"boundary\" \"=-1sRjeMizXVbc5nGIFXbARA==\") NIL NIL)(\"application\" \"pdf\" (\"name\" \"Reminder.pdf\") \"\" NIL \"base64\" 359650 NIL (\"attachment\" (\"filename\" \"Reminder.pdf\" \"size\" \"262820\")) NIL NIL) \"mixed\" (\"boundary\" \"=-EJwVTfPtacyNnTqY4DPQ0A==\") NIL NIL) 0 NIL NIL NIL NIL) \"report\" (\"report-type\" \"delivery-status\" \"boundary\" \"272F16D4031920.1659452466/hermes.gatewaynet.com\") NIL NIL)\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + BodyPart body; + + engine.SetStream (tokenizer); + + try { + body = ImapUtils.ParseBody (engine, "Syntax error in BODYSTRUCTURE: {0}", string.Empty, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing BODYSTRUCTURE failed: {ex}"); + return; + } + + var token = engine.ReadToken (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + AssertParseBadlyFormedBodyStructureWithCompletelyNilBodyParts2 (body); + } + } + } + } + + [Test] + public async Task TestParseBadlyFormedBodyStructureWithCompletelyNilBodyParts2Async () + { + const string text = "((NIL NIL NIL NIL NIL \"7BIT\" 563 NIL NIL NIL NIL)(\"message\" \"delivery-status\" NIL NIL NIL \"7BIT\" 658 NIL NIL NIL NIL)(\"message\" \"rfc822\" NIL NIL NIL \"8bit\" 0 (\"Tue, 2 Aug 2022 15:00:47 +0000\" \"[POSSIBLE SPAM 11.4] Invoices now overdue - 115365#\" ((NIL NIL \"MAILBOX\" \"OUR-DOMAIN\")) NIL NIL ((NIL NIL \"accounts\" \"OTHER-DOMAIN\") (NIL NIL \"safety\" \"OTHER-DOMAIN\") (NIL NIL \"USER\" \"OUR-DOMAIN\")) NIL NIL NIL \"<1IOGPFNLIHU4.377MHPZYJQ6E3@OUR-SERVER>\") (((\"text\" \"plain\" (\"charset\" \"utf-8\") NIL NIL \"8bit\" 597 16 NIL NIL NIL NIL)((\"text\" \"html\" (\"charset\" \"utf-8\") NIL NIL \"7BIT\" 1611 26 NIL NIL NIL NIL)(\"image\" \"png\" (\"name\" \"0.dat\") \"<1KWGPFNLIHU4.4RR7HCVM8MQQ1@OUR-SERVER>\" NIL \"base64\" 14172 NIL (\"inline\" (\"filename\" \"0.dat\")) NIL \"0.dat\")(\"image\" \"png\" (\"name\" \"1.dat\") \"<1KWGPFNLIHU4.UWJ8R86RE2KA2@OUR-SERVER>\" NIL \"base64\" 486 NIL (\"inline\" (\"filename\" \"1.dat\")) NIL \"1.dat\")(\"image\" \"png\" (\"name\" \"2.dat\") \"<1KWGPFNLIHU4.EC7HN124OJC32@OUR-SERVER>\" NIL \"base64\" 506 NIL (\"inline\" (\"filename\" \"2.dat\")) NIL \"2.dat\")(\"image\" \"png\" (\"name\" \"3.dat\") \"<1KWGPFNLIHU4.WM1ALJTG745F1@OUR-SERVER>\" NIL \"base64\" 616 NIL (\"inline\" (\"filename\" \"3.dat\")) NIL \"3.dat\")(\"image\" \"png\" (\"name\" \"4.dat\") \"<1KWGPFNLIHU4.1B42S5EVSF4B2@OUR-SERVER>\" NIL \"base64\" 22470 NIL (\"inline\" (\"filename\" \"4.dat\")) NIL \"4.dat\") \"related\" (\"boundary\" \"=-5nEE2FIlRoeXkJyZAHV8UA==\" \"type\" \"text/html\") NIL NIL) \"alternative\" (\"boundary\" \"=-1sRjeMizXVbc5nGIFXbARA==\") NIL NIL)(\"application\" \"pdf\" (\"name\" \"Reminder.pdf\") \"\" NIL \"base64\" 359650 NIL (\"attachment\" (\"filename\" \"Reminder.pdf\" \"size\" \"262820\")) NIL NIL) \"mixed\" (\"boundary\" \"=-EJwVTfPtacyNnTqY4DPQ0A==\") NIL NIL) 0 NIL NIL NIL NIL) \"report\" (\"report-type\" \"delivery-status\" \"boundary\" \"272F16D4031920.1659452466/hermes.gatewaynet.com\") NIL NIL)\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + BodyPart body; + + engine.SetStream (tokenizer); + + try { + body = await ImapUtils.ParseBodyAsync (engine, "Syntax error in BODYSTRUCTURE: {0}", string.Empty, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing BODYSTRUCTURE failed: {ex}"); + return; + } + + var token = await engine.ReadTokenAsync (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + AssertParseBadlyFormedBodyStructureWithCompletelyNilBodyParts2 (body); + } + } + } + } + + static void AssertParseBadlyFormedBodyStructureWithEmptyParensInsteadOfContentLocation (BodyPart body) + { + Assert.That (body, Is.InstanceOf (), "Body types did not match."); + var multipart = (BodyPartMultipart) body; + + Assert.That (multipart.ContentType.IsMimeType ("multipart", "related"), Is.True, "Content-Type did not match."); + Assert.That (multipart.ContentType.Boundary, Is.EqualTo ("_004_7D3F5AE184118942976793FC500B8F4A402D17DB3PRD0702MB097eu_"), "boundary param did not match"); + Assert.That (multipart.ContentDisposition, Is.Null, "Content-Disposition should be null"); + Assert.That (multipart.ContentLanguage, Has.Length.EqualTo (1), "Content-Language should not be null"); + Assert.That (multipart.ContentLanguage[0], Is.EqualTo ("de-DE"), "Content-Language did not match"); + Assert.That (multipart.ContentLocation, Is.Null, "Content-Location should be null"); + Assert.That (multipart.BodyParts, Has.Count.EqualTo (4), "multipart children did not match"); + + Assert.That (multipart.BodyParts[0], Is.InstanceOf (), "First multipart/related subpart types did not match."); + var text = (BodyPartText) multipart.BodyParts[0]; + Assert.That (text.ContentType.IsMimeType ("text", "html"), Is.True, "Content-Type did not match."); + Assert.That (text.ContentType.Charset, Is.EqualTo ("utf-8"), "Charset param did not match"); + Assert.That (text.ContentDescription, Is.Null, "Content-Description should be null"); + Assert.That (text.ContentDisposition, Is.Null, "Content-Disposition should be null"); + Assert.That (text.ContentId, Is.Null, "Content-Id should be null"); + Assert.That (text.ContentLanguage, Is.Null, "Content-Language should be null"); + Assert.That (text.ContentLocation, Is.Null, "Content-Location should be null"); + Assert.That (text.ContentMd5, Is.Null, "Content-Md5 should be null"); + Assert.That (text.ContentTransferEncoding, Is.EqualTo ("base64"), "Content-Transfer-Encodings did not match"); + Assert.That (text.Octets, Is.EqualTo (38706), "Octets did not match"); + Assert.That (text.Lines, Is.EqualTo (497), "Lines did not match"); + + Assert.That (multipart.BodyParts[1], Is.InstanceOf (), "Second multipart/related subpart types did not match."); + var image1 = (BodyPartBasic) multipart.BodyParts[1]; + Assert.That (image1.ContentType.IsMimeType ("image", "jpeg"), Is.True, "Content-Type did not match."); + Assert.That (image1.ContentType.Name, Is.EqualTo ("image003.jpg"), "Name parameter did not match"); + Assert.That (image1.ContentDescription, Is.EqualTo ("image003.jpg"), "Content-Description should be null"); + Assert.That (image1.ContentDisposition.Disposition, Is.EqualTo ("inline"), "Content-Disposition did not match"); + Assert.That (image1.ContentDisposition.Parameters.ToString (), Is.EqualTo ("; filename=\"image003.jpg\"; size=\"2782\"; creation-date=\"Thu, 22 Mar 2012 13:56:38 GMT\"; modification-date=\"Thu, 22 Mar 2012 13:56:38 GMT\""), "Content-Disposition parameters did not match"); + Assert.That (image1.ContentId, Is.EqualTo (""), "Content-Id did not match"); + Assert.That (image1.ContentLanguage, Is.Null, "Content-Language should be null"); + Assert.That (image1.ContentLocation, Is.Null, "Content-Location should be null"); + Assert.That (image1.ContentMd5, Is.Null, "Content-Md5 should be null"); + Assert.That (image1.ContentTransferEncoding, Is.EqualTo ("base64"), "Content-Transfer-Encodings did not match"); + Assert.That (image1.Octets, Is.EqualTo (3446), "Octets did not match"); + + Assert.That (multipart.BodyParts[2], Is.InstanceOf (), "Third multipart/related subpart types did not match."); + var image2 = (BodyPartBasic) multipart.BodyParts[2]; + Assert.That (image2.ContentType.IsMimeType ("image", "jpeg"), Is.True, "Content-Type did not match."); + Assert.That (image2.ContentType.Name, Is.EqualTo ("image004.jpg"), "Name parameter did not match"); + Assert.That (image2.ContentDescription, Is.EqualTo ("image004.jpg"), "Content-Description should be null"); + Assert.That (image2.ContentDisposition.Disposition, Is.EqualTo ("inline"), "Content-Disposition did not match"); + Assert.That (image2.ContentDisposition.Parameters.ToString (), Is.EqualTo ("; filename=\"image004.jpg\"; size=\"2782\"; creation-date=\"Thu, 22 Mar 2012 13:56:39 GMT\"; modification-date=\"Thu, 22 Mar 2012 13:56:39 GMT\""), "Content-Disposition parameters did not match"); + Assert.That (image2.ContentId, Is.EqualTo (""), "Content-Id did not match"); + Assert.That (image2.ContentLanguage, Is.Null, "Content-Language should be null"); + Assert.That (image2.ContentLocation, Is.Null, "Content-Location should be null"); + Assert.That (image2.ContentMd5, Is.Null, "Content-Md5 should be null"); + Assert.That (image2.ContentTransferEncoding, Is.EqualTo ("base64"), "Content-Transfer-Encodings did not match"); + Assert.That (image2.Octets, Is.EqualTo (3446), "Octets did not match"); + + Assert.That (multipart.BodyParts[3], Is.InstanceOf (), "Fourth multipart/related subpart types did not match."); + var image3 = (BodyPartBasic) multipart.BodyParts[3]; + Assert.That (image3.ContentType.IsMimeType ("image", "jpeg"), Is.True, "Content-Type did not match."); + Assert.That (image3.ContentType.Name, Is.EqualTo ("image005.jpg"), "Name parameter did not match"); + Assert.That (image3.ContentDescription, Is.EqualTo ("image005.jpg"), "Content-Description should be null"); + Assert.That (image3.ContentDisposition.Disposition, Is.EqualTo ("inline"), "Content-Disposition did not match"); + Assert.That (image3.ContentDisposition.Parameters.ToString (), Is.EqualTo ("; filename=\"image005.jpg\"; size=\"2625\"; creation-date=\"Thu, 22 Mar 2012 13:56:39 GMT\"; modification-date=\"Thu, 22 Mar 2012 13:56:39 GMT\""), "Content-Disposition parameters did not match"); + Assert.That (image3.ContentId, Is.EqualTo (""), "Content-Id did not match"); + Assert.That (image3.ContentLanguage, Is.Null, "Content-Language should be null"); + Assert.That (image3.ContentLocation, Is.Null, "Content-Location should be null"); + Assert.That (image3.ContentMd5, Is.Null, "Content-Md5 should be null"); + Assert.That (image3.ContentTransferEncoding, Is.EqualTo ("base64"), "Content-Transfer-Encodings did not match"); + Assert.That (image3.Octets, Is.EqualTo (3232), "Octets did not match"); + } + + [Test] + public void TestParseBadlyFormedBodyStructureWithEmptyParensInsteadOfContentLocation () + { + const string text = "((\"text\" \"html\" (\"charset\" \"utf-8\") NIL NIL \"base64\" 38706 497 NIL NIL NIL ()) (\"image\" \"jpeg\" (\"name\" \"image003.jpg\") \"\" \"image003.jpg\" \"base64\" 3446 NIL (\"inline\" (\"filename\" \"image003.jpg\" \"size\" \"2782\" \"creation-date\" \"Thu, 22 Mar 2012 13:56:38 GMT\" \"modification-date\" \"Thu, 22 Mar 2012 13:56:38 GMT\")) NIL ()) (\"image\" \"jpeg\" (\"name\" \"image004.jpg\") \"\" \"image004.jpg\" \"base64\" 3446 NIL (\"inline\" (\"filename\" \"image004.jpg\" \"size\" \"2782\" \"creation-date\" \"Thu, 22 Mar 2012 13:56:39 GMT\" \"modification-date\" \"Thu, 22 Mar 2012 13:56:39 GMT\")) NIL ()) (\"image\" \"jpeg\" (\"name\" \"image005.jpg\") \"\" \"image005.jpg\" \"base64\" 3232 NIL (\"inline\" (\"filename\" \"image005.jpg\" \"size\" \"2625\" \"creation-date\" \"Thu, 22 Mar 2012 13:56:39 GMT\" \"modification-date\" \"Thu, 22 Mar 2012 13:56:39 GMT\")) NIL ()) \"related\" (\"boundary\" \"_004_7D3F5AE184118942976793FC500B8F4A402D17DB3PRD0702MB097eu_\" \"type\" \"text/html\") NIL (\"de-DE\") NIL)\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + BodyPart body; + + engine.SetStream (tokenizer); + + try { + body = ImapUtils.ParseBody (engine, "Syntax error in BODYSTRUCTURE: {0}", string.Empty, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing BODYSTRUCTURE failed: {ex}"); + return; + } + + var token = engine.ReadToken (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + AssertParseBadlyFormedBodyStructureWithEmptyParensInsteadOfContentLocation (body); + } + } + } + } + + [Test] + public async Task TestParseBadlyFormedBodyStructureWithEmptyParensInsteadOfContentLocationAsync () + { + const string text = "((\"text\" \"html\" (\"charset\" \"utf-8\") NIL NIL \"base64\" 38706 497 NIL NIL NIL ()) (\"image\" \"jpeg\" (\"name\" \"image003.jpg\") \"\" \"image003.jpg\" \"base64\" 3446 NIL (\"inline\" (\"filename\" \"image003.jpg\" \"size\" \"2782\" \"creation-date\" \"Thu, 22 Mar 2012 13:56:38 GMT\" \"modification-date\" \"Thu, 22 Mar 2012 13:56:38 GMT\")) NIL ()) (\"image\" \"jpeg\" (\"name\" \"image004.jpg\") \"\" \"image004.jpg\" \"base64\" 3446 NIL (\"inline\" (\"filename\" \"image004.jpg\" \"size\" \"2782\" \"creation-date\" \"Thu, 22 Mar 2012 13:56:39 GMT\" \"modification-date\" \"Thu, 22 Mar 2012 13:56:39 GMT\")) NIL ()) (\"image\" \"jpeg\" (\"name\" \"image005.jpg\") \"\" \"image005.jpg\" \"base64\" 3232 NIL (\"inline\" (\"filename\" \"image005.jpg\" \"size\" \"2625\" \"creation-date\" \"Thu, 22 Mar 2012 13:56:39 GMT\" \"modification-date\" \"Thu, 22 Mar 2012 13:56:39 GMT\")) NIL ()) \"related\" (\"boundary\" \"_004_7D3F5AE184118942976793FC500B8F4A402D17DB3PRD0702MB097eu_\" \"type\" \"text/html\") NIL (\"de-DE\") NIL)\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + BodyPart body; + + engine.SetStream (tokenizer); + + try { + body = await ImapUtils.ParseBodyAsync (engine, "Syntax error in BODYSTRUCTURE: {0}", string.Empty, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing BODYSTRUCTURE failed: {ex}"); + return; + } + + var token = await engine.ReadTokenAsync (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + AssertParseBadlyFormedBodyStructureWithEmptyParensInsteadOfContentLocation (body); + } + } + } + } + + static void AssertParseBadlyFormedGMailMultipartBodyResponseWithNoChildren (BodyPart body) + { + Assert.That (body, Is.InstanceOf (), "Body types did not match."); + var multipart = (BodyPartMultipart) body; + + Assert.That (multipart.ContentType.IsMimeType ("multipart", "report"), Is.True, "Content-Type did not match."); + Assert.That (multipart.ContentType.Boundary, Is.Null, "boundary should be null"); + Assert.That (multipart.ContentDisposition, Is.Null, "Content-Disposition should be null"); + Assert.That (multipart.ContentLanguage, Is.Null, "Content-Language should be null"); + Assert.That (multipart.ContentLocation, Is.Null, "Content-Location should be null"); + Assert.That (multipart.BodyParts, Has.Count.EqualTo (3), "multipart children did not match"); + + Assert.That (multipart.BodyParts[0], Is.InstanceOf (), "First multipart/report subpart types did not match."); + var text = (BodyPartText) multipart.BodyParts[0]; + Assert.That (text.ContentType.IsMimeType ("text", "plain"), Is.True, "Content-Type did not match."); + Assert.That (text.ContentType.Charset, Is.EqualTo ("windows-1252"), "Charset param did not match"); + Assert.That (text.ContentDescription, Is.Null, "Content-Description should be null"); + Assert.That (text.ContentDisposition, Is.Null, "Content-Disposition should be null"); + Assert.That (text.ContentId, Is.Null, "Content-Id should be null"); + Assert.That (text.ContentLanguage, Is.Null, "Content-Language should be null"); + Assert.That (text.ContentLocation, Is.Null, "Content-Location should be null"); + Assert.That (text.ContentMd5, Is.Null, "Content-Md5 should be null"); + Assert.That (text.ContentTransferEncoding, Is.EqualTo ("QUOTED-PRINTABLE"), "Content-Transfer-Encodings did not match"); + Assert.That (text.Octets, Is.EqualTo (211), "Octets did not match"); + Assert.That (text.Lines, Is.EqualTo (5), "Lines did not match"); + + Assert.That (multipart.BodyParts[1], Is.InstanceOf (), "Second multipart/report subpart types did not match."); + var deliveryStatus = (BodyPartBasic) multipart.BodyParts[1]; + Assert.That (deliveryStatus.ContentType.IsMimeType ("message", "delivery-status"), Is.True, "Content-Type did not match."); + Assert.That (deliveryStatus.ContentDescription, Is.Null, "Content-Description should be null"); + Assert.That (deliveryStatus.ContentDisposition, Is.Null, "Content-Disposition should be null"); + Assert.That (deliveryStatus.ContentId, Is.Null, "Content-Id should be null"); + Assert.That (deliveryStatus.ContentLanguage, Is.Null, "Content-Language should be null"); + Assert.That (deliveryStatus.ContentLocation, Is.Null, "Content-Location should be null"); + Assert.That (deliveryStatus.ContentMd5, Is.Null, "Content-Md5 should be null"); + Assert.That (deliveryStatus.ContentTransferEncoding, Is.EqualTo ("7BIT"), "Content-Transfer-Encodings did not match"); + Assert.That (deliveryStatus.Octets, Is.EqualTo (344), "Octets did not match"); + + Assert.That (multipart.BodyParts[2], Is.InstanceOf (), "Third multipart/report subpart types did not match."); + var rfc822 = (BodyPartMessage) multipart.BodyParts[2]; + Assert.That (rfc822.ContentType.IsMimeType ("message", "rfc822"), Is.True, "Content-Type did not match."); + Assert.That (rfc822.ContentDescription, Is.Null, "Content-Description should be null"); + Assert.That (rfc822.ContentDisposition, Is.Null, "Content-Disposition should be null"); + Assert.That (rfc822.ContentId, Is.Null, "Content-Id should be null"); + Assert.That (rfc822.ContentLanguage, Is.Null, "Content-Language should be null"); + Assert.That (rfc822.ContentLocation, Is.Null, "Content-Location should be null"); + Assert.That (rfc822.ContentMd5, Is.Null, "Content-Md5 should be null"); + Assert.That (rfc822.ContentTransferEncoding, Is.EqualTo ("7BIT"), "Content-Transfer-Encodings did not match"); + Assert.That (rfc822.Octets, Is.EqualTo (2942), "Octets did not match"); + + Assert.That (rfc822.Body, Is.InstanceOf (), "Body of message/rfc822 did not match."); + var alternative = (BodyPartMultipart) rfc822.Body; + Assert.That (alternative.ContentType.IsMimeType ("multipart", "alternative"), Is.True, "Content-Type did not match."); + Assert.That (alternative.ContentType.Boundary, Is.Null, "boundary should be null"); + Assert.That (alternative.ContentDisposition, Is.Null, "Content-Disposition should be null"); + Assert.That (alternative.ContentLanguage, Is.Null, "Content-Language should be null"); + Assert.That (alternative.ContentLocation, Is.Null, "Content-Location should be null"); + Assert.That (alternative.BodyParts, Has.Count.EqualTo (0), "multipart children did not match"); + } + + // issue 1841 + [Test] + public void TestParseBadlyFormedGMailMultipartBodyResponseWithNoChildren () + { + const string text = "((\"TEXT\" \"PLAIN\" (\"CHARSET\" \"windows-1252\") NIL NIL \"QUOTED-PRINTABLE\" 211 5)(\"MESSAGE\" \"DELIVERY-STATUS\" NIL NIL NIL \"7BIT\" 344)(\"MESSAGE\" \"RFC822\" NIL NIL NIL \"7BIT\" 2942 (\"Sat, 3 Jan 2015 19:26:15 +0100\" \"Re: MPG\" ((\"c p\" NIL \"---.---87\" \"gmail.com\")) ((\"c p\" NIL \"---.---87\" \"gmail.com\")) ((\"c p\" NIL \"---.---87\" \"gmail.com\")) ((\"am sa[cft]\" NIL \"sm\" \"cft.ce.fr\")) NIL NIL \"\" \"\") (\"ALTERNATIVE\") 51) \"REPORT\")\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + BodyPart body; + + engine.QuirksMode = ImapQuirksMode.GMail; + engine.SetStream (tokenizer); + + try { + body = ImapUtils.ParseBody (engine, "Syntax error in BODY: {0}", string.Empty, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing BODY failed: {ex}"); + return; + } + + var token = engine.ReadToken (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + AssertParseBadlyFormedGMailMultipartBodyResponseWithNoChildren (body); + } + } + } + } + + // issue 1841 + [Test] + public async Task TestParseBadlyFormedGMailMultipartBodyResponseWithNoChildrenAsync () + { + const string text = "((\"TEXT\" \"PLAIN\" (\"CHARSET\" \"windows-1252\") NIL NIL \"QUOTED-PRINTABLE\" 211 5)(\"MESSAGE\" \"DELIVERY-STATUS\" NIL NIL NIL \"7BIT\" 344)(\"MESSAGE\" \"RFC822\" NIL NIL NIL \"7BIT\" 2942 (\"Sat, 3 Jan 2015 19:26:15 +0100\" \"Re: MPG\" ((\"c p\" NIL \"---.---87\" \"gmail.com\")) ((\"c p\" NIL \"---.---87\" \"gmail.com\")) ((\"c p\" NIL \"---.---87\" \"gmail.com\")) ((\"am sa[cft]\" NIL \"sm\" \"cft.ce.fr\")) NIL NIL \"\" \"\") (\"ALTERNATIVE\") 51) \"REPORT\")\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + BodyPart body; + + engine.QuirksMode = ImapQuirksMode.GMail; + engine.SetStream (tokenizer); + + try { + body = await ImapUtils.ParseBodyAsync (engine, "Syntax error in BODY: {0}", string.Empty, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing BODY failed: {ex}"); + return; + } + + var token = await engine.ReadTokenAsync (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + AssertParseBadlyFormedGMailMultipartBodyResponseWithNoChildren (body); + } + } + } + } + + [Test] + public void TestParseExampleThreads () + { + const string text = "(2)(3 6 (4 23)(44 7 96))\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + var threads = new List (); + + engine.SetStream (tokenizer); + + try { + ImapUtils.ParseThreads (engine, 0, threads, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing THREAD response failed: {ex}"); + return; + } + + var token = engine.ReadToken (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (threads, Has.Count.EqualTo (2), "Expected 2 threads."); + + Assert.That (threads[0].UniqueId.Value.Id, Is.EqualTo ((uint) 2)); + Assert.That (threads[1].UniqueId.Value.Id, Is.EqualTo ((uint) 3)); + + var branches = threads[1].Children.ToArray (); + Assert.That (branches, Has.Length.EqualTo (1), "Expected 1 child."); + Assert.That (branches[0].UniqueId.Value.Id, Is.EqualTo ((uint) 6)); + + branches = branches[0].Children.ToArray (); + Assert.That (branches, Has.Length.EqualTo (2), "Expected 2 branches."); + + Assert.That (branches[0].UniqueId.Value.Id, Is.EqualTo ((uint) 4)); + Assert.That (branches[1].UniqueId.Value.Id, Is.EqualTo ((uint) 44)); + + var children = branches[0].Children.ToArray (); + Assert.That (children, Has.Length.EqualTo (1), "Expected 1 child."); + Assert.That (children[0].UniqueId.Value.Id, Is.EqualTo ((uint) 23)); + Assert.That (children[0].Children, Is.Empty, "Expected no children."); + + children = branches[1].Children.ToArray (); + Assert.That (children, Has.Length.EqualTo (1), "Expected 1 child."); + Assert.That (children[0].UniqueId.Value.Id, Is.EqualTo ((uint) 7)); + + children = children[0].Children.ToArray (); + Assert.That (children, Has.Length.EqualTo (1), "Expected 1 child."); + Assert.That (children[0].UniqueId.Value.Id, Is.EqualTo ((uint) 96)); + Assert.That (children[0].Children, Is.Empty, "Expected no children."); + } + } + } + } + + [Test] + public async Task TestParseExampleThreadsAsync () + { + const string text = "(2)(3 6 (4 23)(44 7 96))\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + var threads = new List (); + + engine.SetStream (tokenizer); + + try { + await ImapUtils.ParseThreadsAsync (engine, 0, threads, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing THREAD response failed: {ex}"); + return; + } + + var token = await engine.ReadTokenAsync (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (threads, Has.Count.EqualTo (2), "Expected 2 threads."); + + Assert.That (threads[0].UniqueId.Value.Id, Is.EqualTo ((uint) 2)); + Assert.That (threads[1].UniqueId.Value.Id, Is.EqualTo ((uint) 3)); + + var branches = threads[1].Children.ToArray (); + Assert.That (branches, Has.Length.EqualTo (1), "Expected 1 child."); + Assert.That (branches[0].UniqueId.Value.Id, Is.EqualTo ((uint) 6)); + + branches = branches[0].Children.ToArray (); + Assert.That (branches, Has.Length.EqualTo (2), "Expected 2 branches."); + + Assert.That (branches[0].UniqueId.Value.Id, Is.EqualTo ((uint) 4)); + Assert.That (branches[1].UniqueId.Value.Id, Is.EqualTo ((uint) 44)); + + var children = branches[0].Children.ToArray (); + Assert.That (children, Has.Length.EqualTo (1), "Expected 1 child."); + Assert.That (children[0].UniqueId.Value.Id, Is.EqualTo ((uint) 23)); + Assert.That (children[0].Children, Is.Empty, "Expected no children."); + + children = branches[1].Children.ToArray (); + Assert.That (children, Has.Length.EqualTo (1), "Expected 1 child."); + Assert.That (children[0].UniqueId.Value.Id, Is.EqualTo ((uint) 7)); + + children = children[0].Children.ToArray (); + Assert.That (children, Has.Length.EqualTo (1), "Expected 1 child."); + Assert.That (children[0].UniqueId.Value.Id, Is.EqualTo ((uint) 96)); + Assert.That (children[0].Children, Is.Empty, "Expected no children."); + } + } + } + } + + [Test] + public void TestParseLongDovecotExampleThread () + { + const string text = "(3 4 5 6 7)(1)((2)(8)(15))(9)(16)(10)(11)(12 13)(14)(17)(18)(19)(20)(21)(22)(23)(24)(25 (26)(29 39)(31)(32))(27)(28)(38 35)(30 33 34)(37)(36)(40)(41)((42 43)(44)(48)(49)(50)(51 52))(45)((46)(55))(47)(53)(54)(56)(57 (58)(59)(60)(63))((61)(62))(64)(65)(70)((66)(67)(68)(69)(71))(72 73 (74)(75)(76 77))\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + var threads = new List (); + + engine.SetStream (tokenizer); + + try { + ImapUtils.ParseThreads (engine, 0, threads, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing THREAD response failed: {ex}"); + return; + } + + var token = engine.ReadToken (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (threads, Has.Count.EqualTo (40), "Expected 40 threads."); + + Assert.That (threads[0].UniqueId.Value.Id, Is.EqualTo ((uint) 3)); + Assert.That (threads[1].UniqueId.Value.Id, Is.EqualTo ((uint) 1)); + //Assert.That (threads[2].UniqueId.Value.Id, Is.EqualTo ((uint) 0)); + Assert.That (threads[2].UniqueId.HasValue, Is.False); + + var branches = threads[2].Children.ToArray (); + Assert.That (branches, Has.Length.EqualTo (3), "Expected 3 children."); + } + } + } + } + + [Test] + public async Task TestParseLongDovecotExampleThreadAsync () + { + const string text = "(3 4 5 6 7)(1)((2)(8)(15))(9)(16)(10)(11)(12 13)(14)(17)(18)(19)(20)(21)(22)(23)(24)(25 (26)(29 39)(31)(32))(27)(28)(38 35)(30 33 34)(37)(36)(40)(41)((42 43)(44)(48)(49)(50)(51 52))(45)((46)(55))(47)(53)(54)(56)(57 (58)(59)(60)(63))((61)(62))(64)(65)(70)((66)(67)(68)(69)(71))(72 73 (74)(75)(76 77))\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + var threads = new List (); + + engine.SetStream (tokenizer); + + try { + await ImapUtils.ParseThreadsAsync (engine, 0, threads, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing THREAD response failed: {ex}"); + return; + } + + var token = await engine.ReadTokenAsync (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (threads, Has.Count.EqualTo (40), "Expected 40 threads."); + + Assert.That (threads[0].UniqueId.Value.Id, Is.EqualTo ((uint) 3)); + Assert.That (threads[1].UniqueId.Value.Id, Is.EqualTo ((uint) 1)); + //Assert.That (threads[2].UniqueId.Value.Id, Is.EqualTo ((uint) 0)); + Assert.That (threads[2].UniqueId.HasValue, Is.False); + + var branches = threads[2].Children.ToArray (); + Assert.That (branches, Has.Length.EqualTo (3), "Expected 3 children."); + } + } + } + } + + [Test] + public void TestParseShortDovecotExampleThread () + { + const string text = "((352)(381))\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + var threads = new List (); + + engine.SetStream (tokenizer); + + try { + ImapUtils.ParseThreads (engine, 0, threads, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing THREAD response failed: {ex}"); + return; + } + + var token = engine.ReadToken (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (threads, Has.Count.EqualTo (1), "Expected 1 thread."); + + //Assert.That (threads[0].UniqueId.Value.Id, Is.EqualTo ((uint) 0)); + Assert.That (threads[0].UniqueId.HasValue, Is.False); + + var children = threads[0].Children; + Assert.That (children, Has.Count.EqualTo (2), "Expected 2 children."); + + Assert.That (children[0].UniqueId.Value.Id, Is.EqualTo ((uint) 352)); + Assert.That (children[1].UniqueId.Value.Id, Is.EqualTo ((uint) 381)); + } + } + } + } + + [Test] + public async Task TestParseShortDovecotExampleThreadAsync () + { + const string text = "((352)(381))\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + var threads = new List (); + + engine.SetStream (tokenizer); + + try { + await ImapUtils.ParseThreadsAsync (engine, 0, threads, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing THREAD response failed: {ex}"); + return; + } + + var token = await engine.ReadTokenAsync (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (threads, Has.Count.EqualTo (1), "Expected 1 thread."); + + //Assert.That (threads[0].UniqueId.Value.Id, Is.EqualTo ((uint) 0)); + Assert.That (threads[0].UniqueId.HasValue, Is.False); + + var children = threads[0].Children; + Assert.That (children, Has.Count.EqualTo (2), "Expected 2 children."); + + Assert.That (children[0].UniqueId.Value.Id, Is.EqualTo ((uint) 352)); + Assert.That (children[1].UniqueId.Value.Id, Is.EqualTo ((uint) 381)); + } + } + } + } + + [Test] + public void TestFormatAnnotations () + { + var annotations = new List (); + var command = new StringBuilder ("STORE "); + var args = new List (); + + ImapUtils.FormatAnnotations (command, annotations, args, false); + Assert.That (command.ToString (), Is.EqualTo ("STORE "), "empty collection"); + + annotations.Add (new Annotation (AnnotationEntry.AltSubject)); + + ImapUtils.FormatAnnotations (command, annotations, args, false); + Assert.That (command.ToString (), Is.EqualTo ("STORE "), "annotation w/o properties"); + Assert.Throws (() => ImapUtils.FormatAnnotations (command, annotations, args, true)); + + command.Clear (); + command.Append ("STORE "); + annotations[0].Properties.Add (AnnotationAttribute.SharedValue, "This is an alternate subject."); + ImapUtils.FormatAnnotations (command, annotations, args, true); + Assert.That (command.ToString (), Is.EqualTo ("STORE ANNOTATION (/altsubject (value.shared %S))")); + Assert.That (args, Has.Count.EqualTo (1), "args"); + Assert.That (args[0], Is.EqualTo ("This is an alternate subject."), "args[0]"); + } + + [Test] + public void TestParseAnnotationsExample1 () + { + const string text = "(/comment (value.priv \"My comment\" value.shared NIL))\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + IList annotations; + + engine.SetStream (tokenizer); + + try { + annotations = ImapUtils.ParseAnnotations (engine, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing ANNOTATION response failed: {ex}"); + return; + } + + var token = engine.ReadToken (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (annotations, Has.Count.EqualTo (1), "Count"); + Assert.That (annotations[0].Entry, Is.EqualTo (AnnotationEntry.Comment), "Entry"); + Assert.That (annotations[0].Properties, Has.Count.EqualTo (2), "Properties.Count"); + Assert.That (annotations[0].Properties[AnnotationAttribute.PrivateValue], Is.EqualTo ("My comment"), "value.priv"); + Assert.That (annotations[0].Properties[AnnotationAttribute.SharedValue], Is.EqualTo (null), "value.shared"); + } + } + } + } + + [Test] + public async Task TestParseAnnotationsExample1Async () + { + const string text = "(/comment (value.priv \"My comment\" value.shared NIL))\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + IList annotations; + + engine.SetStream (tokenizer); + + try { + annotations = await ImapUtils.ParseAnnotationsAsync (engine, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing ANNOTATION response failed: {ex}"); + return; + } + + var token = await engine.ReadTokenAsync (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (annotations, Has.Count.EqualTo (1), "Count"); + Assert.That (annotations[0].Entry, Is.EqualTo (AnnotationEntry.Comment), "Entry"); + Assert.That (annotations[0].Properties, Has.Count.EqualTo (2), "Properties.Count"); + Assert.That (annotations[0].Properties[AnnotationAttribute.PrivateValue], Is.EqualTo ("My comment"), "value.priv"); + Assert.That (annotations[0].Properties[AnnotationAttribute.SharedValue], Is.EqualTo (null), "value.shared"); + } + } + } + } + + [Test] + public void TestParseAnnotationsExample2 () + { + const string text = "(/comment (value.priv \"My comment\" value.shared NIL) /altsubject (value.priv \"My subject\" value.shared NIL))\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + IList annotations; + + engine.SetStream (tokenizer); + + try { + annotations = ImapUtils.ParseAnnotations (engine, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing ANNOTATION response failed: {ex}"); + return; + } + + var token = engine.ReadToken (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (annotations, Has.Count.EqualTo (2), "Count"); + Assert.That (annotations[0].Entry, Is.EqualTo (AnnotationEntry.Comment), "annotations[0].Entry"); + Assert.That (annotations[0].Properties, Has.Count.EqualTo (2), "annotations[0].Properties.Count"); + Assert.That (annotations[0].Properties[AnnotationAttribute.PrivateValue], Is.EqualTo ("My comment"), "annotations[0] value.priv"); + Assert.That (annotations[0].Properties[AnnotationAttribute.SharedValue], Is.EqualTo (null), "annotations[0] value.shared"); + Assert.That (annotations[1].Entry, Is.EqualTo (AnnotationEntry.AltSubject), "annotations[1].Entry"); + Assert.That (annotations[1].Properties, Has.Count.EqualTo (2), "annotations[1].Properties.Count"); + Assert.That (annotations[1].Properties[AnnotationAttribute.PrivateValue], Is.EqualTo ("My subject"), "annotations[1] value.priv"); + Assert.That (annotations[1].Properties[AnnotationAttribute.SharedValue], Is.EqualTo (null), "annotations[1] value.shared"); + } + } + } + } + + [Test] + public async Task TestParseAnnotationsExample2Async () + { + const string text = "(/comment (value.priv \"My comment\" value.shared NIL) /altsubject (value.priv \"My subject\" value.shared NIL))\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + IList annotations; + + engine.SetStream (tokenizer); + + try { + annotations = await ImapUtils.ParseAnnotationsAsync (engine, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing ANNOTATION response failed: {ex}"); + return; + } + + var token = await engine.ReadTokenAsync (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (annotations, Has.Count.EqualTo (2), "Count"); + Assert.That (annotations[0].Entry, Is.EqualTo (AnnotationEntry.Comment), "annotations[0].Entry"); + Assert.That (annotations[0].Properties, Has.Count.EqualTo (2), "annotations[0].Properties.Count"); + Assert.That (annotations[0].Properties[AnnotationAttribute.PrivateValue], Is.EqualTo ("My comment"), "annotations[0] value.priv"); + Assert.That (annotations[0].Properties[AnnotationAttribute.SharedValue], Is.EqualTo (null), "annotations[0] value.shared"); + Assert.That (annotations[1].Entry, Is.EqualTo (AnnotationEntry.AltSubject), "annotations[1].Entry"); + Assert.That (annotations[1].Properties, Has.Count.EqualTo (2), "annotations[1].Properties.Count"); + Assert.That (annotations[1].Properties[AnnotationAttribute.PrivateValue], Is.EqualTo ("My subject"), "annotations[1] value.priv"); + Assert.That (annotations[1].Properties[AnnotationAttribute.SharedValue], Is.EqualTo (null), "annotations[1] value.shared"); + } + } + } + } + + [Test] + public void TestParseAnnotationsExample3 () + { + const string text = "(/comment (value.priv \"My comment\" value.shared NIL size.priv \"10\" size.shared \"0\"))\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + IList annotations; + + engine.SetStream (tokenizer); + + try { + annotations = ImapUtils.ParseAnnotations (engine, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing ANNOTATION response failed: {ex}"); + return; + } + + var token = engine.ReadToken (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (annotations, Has.Count.EqualTo (1), "Count"); + Assert.That (annotations[0].Entry, Is.EqualTo (AnnotationEntry.Comment), "annotations[0].Entry"); + Assert.That (annotations[0].Properties, Has.Count.EqualTo (4), "annotations[0].Properties.Count"); + Assert.That (annotations[0].Properties[AnnotationAttribute.PrivateValue], Is.EqualTo ("My comment"), "annotations[0] value.priv"); + Assert.That (annotations[0].Properties[AnnotationAttribute.SharedValue], Is.EqualTo (null), "annotations[0] value.shared"); + Assert.That (annotations[0].Properties[AnnotationAttribute.PrivateSize], Is.EqualTo ("10"), "annotations[0] size.priv"); + Assert.That (annotations[0].Properties[AnnotationAttribute.SharedSize], Is.EqualTo ("0"), "annotations[0] size.shared"); + } + } + } + } + + [Test] + public async Task TestParseAnnotationsExample3Async () + { + const string text = "(/comment (value.priv \"My comment\" value.shared NIL size.priv \"10\" size.shared \"0\"))\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + IList annotations; + + engine.SetStream (tokenizer); + + try { + annotations = await ImapUtils.ParseAnnotationsAsync (engine, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing ANNOTATION response failed: {ex}"); + return; + } + + var token = await engine.ReadTokenAsync (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (annotations, Has.Count.EqualTo (1), "Count"); + Assert.That (annotations[0].Entry, Is.EqualTo (AnnotationEntry.Comment), "annotations[0].Entry"); + Assert.That (annotations[0].Properties, Has.Count.EqualTo (4), "annotations[0].Properties.Count"); + Assert.That (annotations[0].Properties[AnnotationAttribute.PrivateValue], Is.EqualTo ("My comment"), "annotations[0] value.priv"); + Assert.That (annotations[0].Properties[AnnotationAttribute.SharedValue], Is.EqualTo (null), "annotations[0] value.shared"); + Assert.That (annotations[0].Properties[AnnotationAttribute.PrivateSize], Is.EqualTo ("10"), "annotations[0] size.priv"); + Assert.That (annotations[0].Properties[AnnotationAttribute.SharedSize], Is.EqualTo ("0"), "annotations[0] size.shared"); + } + } + } + } + + [Test] + public void TestParseFlagsList () + { + const string text = "(\\Answered \\Flagged \\Deleted \\Seen \\Draft + TAG TAG2 a b c d e f g h i j k l m n o p q r s t u v w x y z)\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + var keywords = new HashSet (); + MessageFlags flags; + + engine.SetStream (tokenizer); + + try { + flags = ImapUtils.ParseFlagsList (engine, "INBOX", keywords, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing FLAGS response failed: {ex}"); + return; + } + + var token = engine.ReadToken (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (flags, Is.EqualTo (MessageFlags.Answered | MessageFlags.Flagged | MessageFlags.Deleted | MessageFlags.Seen | MessageFlags.Draft), "message flags"); + + Assert.That (keywords, Has.Count.EqualTo (29), "keywords.Count"); + Assert.That (keywords.Contains ("+"), Is.True, "Contains +"); + Assert.That (keywords.Contains ("TAG"), Is.True, "Contains TAG"); + Assert.That (keywords.Contains ("TAG2"), Is.True, "Contains TAG2"); + + for (char c = 'a'; c <= 'z'; c++) + Assert.That (keywords.Contains (c.ToString ()), Is.True, $"Contains {c}"); + } + } + } + } + + [Test] + public async Task TestParseFlagsListAsync () + { + const string text = "(\\Answered \\Flagged \\Deleted \\Seen \\Draft + TAG TAG2 a b c d e f g h i j k l m n o p q r s t u v w x y z)\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (null)) { + var keywords = new HashSet (); + MessageFlags flags; + + engine.SetStream (tokenizer); + + try { + flags = await ImapUtils.ParseFlagsListAsync (engine, "INBOX", keywords, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing FLAGS response failed: {ex}"); + return; + } + + var token = await engine.ReadTokenAsync (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (flags, Is.EqualTo (MessageFlags.Answered | MessageFlags.Flagged | MessageFlags.Deleted | MessageFlags.Seen | MessageFlags.Draft), "message flags"); + + Assert.That (keywords, Has.Count.EqualTo (29), "keywords.Count"); + Assert.That (keywords.Contains ("+"), Is.True, "Contains +"); + Assert.That (keywords.Contains ("TAG"), Is.True, "Contains TAG"); + Assert.That (keywords.Contains ("TAG2"), Is.True, "Contains TAG2"); + + for (char c = 'a'; c <= 'z'; c++) + Assert.That (keywords.Contains (c.ToString ()), Is.True, $"Contains {c}"); + } + } + } + } + + ImapFolder CreateImapFolder (ImapFolderConstructorArgs args) + { + return new ImapFolder (args); + } + + // Tests the work-around for issue #945 + [Test] + public void TestParseFolderListWithFolderNameContainingUnquotedTabs () + { + const string text = " (\\HasNoChildren) \"/\" INBOX/Da\tOggetto\tRicevuto\tDimensione\tCategorie\t\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (CreateImapFolder)) { + var list = new List (); + + engine.QuirksMode = ImapQuirksMode.Exchange; + engine.SetStream (tokenizer); + + try { + ImapUtils.ParseFolderList (engine, list, false, false, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing LIST response failed: {ex}"); + return; + } + + var token = engine.ReadToken (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (list, Has.Count.EqualTo (1), "Count"); + Assert.That (list[0].Name, Is.EqualTo ("Da\tOggetto\tRicevuto\tDimensione\tCategorie\t"), "Name"); + } + } + } + } + + [Test] + public async Task TestParseFolderListWithFolderNameContainingUnquotedTabsAsync () + { + const string text = " (\\HasNoChildren) \"/\" INBOX/Da\tOggetto\tRicevuto\tDimensione\tCategorie\t\r\n"; + + using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { + using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { + using (var engine = new ImapEngine (CreateImapFolder)) { + var list = new List (); + + engine.QuirksMode = ImapQuirksMode.Exchange; + engine.SetStream (tokenizer); + + try { + await ImapUtils.ParseFolderListAsync (engine, list, false, false, CancellationToken.None); + } catch (Exception ex) { + Assert.Fail ($"Parsing LIST response failed: {ex}"); + return; + } + + var token = await engine.ReadTokenAsync (CancellationToken.None); + Assert.That (token.Type, Is.EqualTo (ImapTokenType.Eoln), $"Expected new-line, but got: {token}"); + + Assert.That (list, Has.Count.EqualTo (1), "Count"); + Assert.That (list[0].Name, Is.EqualTo ("Da\tOggetto\tRicevuto\tDimensione\tCategorie\t"), "Name"); + } + } + } + } + + [Test] + [TestCase (2023, 5, 14, 12, 30, 45, -4, -30, "14-May-2023 12:30:45 -0430")] // Sunday + [TestCase (2023, 5, 15, 12, 30, 45, -3, -30, "15-May-2023 12:30:45 -0330")] // Monday + [TestCase (2023, 5, 16, 12, 30, 45, -2, 0, "16-May-2023 12:30:45 -0200")] // Tuesday + [TestCase (2023, 5, 17, 12, 30, 45, -1, 0, "17-May-2023 12:30:45 -0100")] // Wednesday + public void TestFormatInternalDateNegativeOffsets (int year, int month, int day, int hour, int minute, int second, int offsetHours, int offsetMinutes, string expected) + { + var date = new DateTimeOffset (year, month, day, hour, minute, second, new TimeSpan (offsetHours, offsetMinutes, 0)); + + string formattedInternalDate = ImapUtils.FormatInternalDate (date); + + Assert.That (formattedInternalDate, Is.EqualTo (expected), $"Expected {expected} but got {formattedInternalDate} for date {date}."); + } + + [Test] + [TestCase (2023, 5, 18, 12, 30, 45, 1, 0, "18-May-2023 12:30:45 +0100")] // Thursday + [TestCase (2023, 5, 19, 12, 30, 45, 4, 30, "19-May-2023 12:30:45 +0430")] // Friday + [TestCase (2023, 5, 20, 12, 30, 45, 9, 30, "20-May-2023 12:30:45 +0930")] // Saturday + [TestCase (2023, 5, 21, 12, 30, 45, 12, 0, "21-May-2023 12:30:45 +1200")] // Sunday + public void TestFormatInternalDatePositiveOffsets (int year, int month, int day, int hour, int minute, int second, int offsetHours, int offsetMinutes, string expected) + { + var date = new DateTimeOffset (year, month, day, hour, minute, second, new TimeSpan (offsetHours, offsetMinutes, 0)); + + string formattedInternalDate = ImapUtils.FormatInternalDate (date); + + Assert.That (formattedInternalDate, Is.EqualTo (expected), $"Expected {expected} but got {formattedInternalDate} for date {date}."); + } + + [Test] + [TestCase (2023, 5, 22, 12, 30, 45, 0, 0, "22-May-2023 12:30:45 +0000")] // Monday + public void TestFormatInternalDateZeroOffset (int year, int month, int day, int hour, int minute, int second, int offsetHours, int offsetMinutes, string expected) + { + var date = new DateTimeOffset (year, month, day, hour, minute, second, new TimeSpan (offsetHours, offsetMinutes, 0)); + + string formattedInternalDate = ImapUtils.FormatInternalDate (date); + + Assert.That (formattedInternalDate, Is.EqualTo (expected), $"Expected {expected} but got {formattedInternalDate} for date {date}."); + } + + [Test] + [TestCase (2023, 5, 23, 23, 59, 59, 2, 30, "23-May-2023 23:59:59 +0230")] // Tuesday + [TestCase (2023, 5, 24, 0, 0, 0, -4, -30, "24-May-2023 00:00:00 -0430")] // Wednesday + public void TestFormatInternalDateEdgeCases (int year, int month, int day, int hour, int minute, int second, int offsetHours, int offsetMinutes, string expected) + { + var date = new DateTimeOffset (year, month, day, hour, minute, second, new TimeSpan (offsetHours, offsetMinutes, 0)); + + string formattedInternalDate = ImapUtils.FormatInternalDate (date); + + Assert.That (formattedInternalDate, Is.EqualTo (expected), $"Expected {expected} but got {formattedInternalDate} for date {date}."); + } } } diff --git a/UnitTests/Net/Imap/Resources/acl/authenticate.txt b/UnitTests/Net/Imap/Resources/acl/authenticate.txt index e2ce55db38..52e0c92388 100644 --- a/UnitTests/Net/Imap/Resources/acl/authenticate.txt +++ b/UnitTests/Net/Imap/Resources/acl/authenticate.txt @@ -1,2 +1,2 @@ * CAPABILITY IMAP4rev1 UNSELECT IDLE NAMESPACE QUOTA ID XLIST CHILDREN X-GM-EXT-1 UIDPLUS COMPRESS=DEFLATE ENABLE MOVE CONDSTORE ESEARCH UTF8=ACCEPT LIST-EXTENDED LIST-STATUS LITERAL- APPENDLIMIT=35651584 ACL RIGHTS=texk -A00000001 OK username authenticated (Success) +A######## OK username authenticated (Success) diff --git a/UnitTests/Net/Imap/Resources/acl/capability.txt b/UnitTests/Net/Imap/Resources/acl/capability.txt index 3f313ebaa0..d08b2f8f93 100644 --- a/UnitTests/Net/Imap/Resources/acl/capability.txt +++ b/UnitTests/Net/Imap/Resources/acl/capability.txt @@ -1,2 +1,2 @@ * CAPABILITY IMAP4rev1 UNSELECT IDLE NAMESPACE QUOTA ID XLIST CHILDREN X-GM-EXT-1 XYZZY SASL-IR AUTH=XOAUTH AUTH=XOAUTH2 AUTH=PLAIN AUTH=PLAIN-CLIENTTOKEN ACL RIGHTS=texk -A00000000 OK Thats all she wrote! i9if7359725qay.199 +A######## OK Thats all she wrote! i9if7359725qay.199 diff --git a/UnitTests/Net/Imap/Resources/acl/getacl.txt b/UnitTests/Net/Imap/Resources/acl/getacl.txt index 3cc9f21172..c8362f7cb9 100644 --- a/UnitTests/Net/Imap/Resources/acl/getacl.txt +++ b/UnitTests/Net/Imap/Resources/acl/getacl.txt @@ -1,2 +1,2 @@ * ACL INBOX Fred rwipslxetad Chris lrswi -A00000005 OK GETACL completed. +A######## OK GETACL completed. diff --git a/UnitTests/Net/Imap/Resources/acl/listrights.txt b/UnitTests/Net/Imap/Resources/acl/listrights.txt index 38e47b316a..01fd3e59ae 100644 --- a/UnitTests/Net/Imap/Resources/acl/listrights.txt +++ b/UnitTests/Net/Imap/Resources/acl/listrights.txt @@ -1,2 +1,2 @@ * LISTRIGHTS INBOX anyone "" l r s w i p k x t e c d a 0 1 2 3 4 5 6 7 8 9 -A00000006 OK LISTRIGHTS completed. +A######## OK LISTRIGHTS completed. diff --git a/UnitTests/Net/Imap/Resources/acl/myrights.txt b/UnitTests/Net/Imap/Resources/acl/myrights.txt index e3add3b18c..b9e3b7ef94 100644 --- a/UnitTests/Net/Imap/Resources/acl/myrights.txt +++ b/UnitTests/Net/Imap/Resources/acl/myrights.txt @@ -1,2 +1,2 @@ * MYRIGHTS INBOX rwiptsldaex -A00000007 OK MYRIGHTS completed. +A######## OK MYRIGHTS completed. diff --git a/UnitTests/Net/Imap/Resources/common/basic-greeting.txt b/UnitTests/Net/Imap/Resources/common/basic-greeting.txt new file mode 100644 index 0000000000..646f85c067 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/common/basic-greeting.txt @@ -0,0 +1 @@ +* OK IMAP4 server ready. diff --git a/UnitTests/Net/Imap/Resources/common/capability.txt b/UnitTests/Net/Imap/Resources/common/capability.txt new file mode 100644 index 0000000000..f4db1d2ee7 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/common/capability.txt @@ -0,0 +1,2 @@ +* CAPABILITY IMAP4rev1 +A######## OK CAPABILITY Complete diff --git a/UnitTests/Net/Imap/Resources/common/fetch-annotations.txt b/UnitTests/Net/Imap/Resources/common/fetch-annotations.txt new file mode 100644 index 0000000000..1299245415 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/common/fetch-annotations.txt @@ -0,0 +1,4 @@ +* 1 FETCH (UID 1 ANNOTATION (/comment (value.priv "My comment" value.shared NIL))) +* 2 FETCH (UID 2 ANNOTATION (/comment (value.priv "My comment" value.shared NIL) /altsubject (value.priv "My subject" value.shared NIL))) +* 3 FETCH (UID 3 ANNOTATION (/comment (value.priv "My comment" value.shared NIL size.priv "10" size.shared "0"))) +A######## OK Success diff --git a/UnitTests/Net/Imap/Resources/common/getquota-no-root.txt b/UnitTests/Net/Imap/Resources/common/getquota-no-root.txt index 6072bceec0..2f228bcbab 100644 --- a/UnitTests/Net/Imap/Resources/common/getquota-no-root.txt +++ b/UnitTests/Net/Imap/Resources/common/getquota-no-root.txt @@ -1,3 +1,3 @@ * QUOTAROOT INBOX storage=0 * QUOTA storage=0 (STORAGE 28257 256000) -A00000005 OK Getquotaroot completed (0.000 + 0.000 secs). +A######## OK Getquotaroot completed (0.000 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/common/getquota.txt b/UnitTests/Net/Imap/Resources/common/getquota.txt index 6072ba3355..d80349ba9a 100644 --- a/UnitTests/Net/Imap/Resources/common/getquota.txt +++ b/UnitTests/Net/Imap/Resources/common/getquota.txt @@ -1,3 +1,3 @@ * QUOTAROOT "INBOX" "" * QUOTA "" (STORAGE 3783 15728640) -A00000006 OK Success +A######## OK Success diff --git a/UnitTests/Net/Imap/Resources/common/id.txt b/UnitTests/Net/Imap/Resources/common/id.txt index 1857e12b98..65b57d952a 100644 --- a/UnitTests/Net/Imap/Resources/common/id.txt +++ b/UnitTests/Net/Imap/Resources/common/id.txt @@ -1,2 +1,2 @@ * ID ("name" "GImap" "vendor" "Google, Inc." "support-url" "http://support.google.com/mail" "version" "gmail_imap_150623.03_p1" "remote-host" "127.0.0.1") -A00000005 OK Success +A######## OK Success diff --git a/UnitTests/Net/Imap/Resources/common/list-inbox.txt b/UnitTests/Net/Imap/Resources/common/list-inbox.txt new file mode 100644 index 0000000000..0992dcad5a --- /dev/null +++ b/UnitTests/Net/Imap/Resources/common/list-inbox.txt @@ -0,0 +1,2 @@ +* LIST () "/" INBOX +A######## OK LIST Completed diff --git a/UnitTests/Net/Imap/Resources/common/list-literal-subfolders.txt b/UnitTests/Net/Imap/Resources/common/list-literal-subfolders.txt new file mode 100644 index 0000000000..2a4b487839 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/common/list-literal-subfolders.txt @@ -0,0 +1,4 @@ +* LIST () "/" INBOX +* LIST (\HasNoChildren) "/" {19} +Literal Folder Name +A######## OK LIST Completed diff --git a/UnitTests/Net/Imap/Resources/common/list-namespace.txt b/UnitTests/Net/Imap/Resources/common/list-namespace.txt new file mode 100644 index 0000000000..c061aa090a --- /dev/null +++ b/UnitTests/Net/Imap/Resources/common/list-namespace.txt @@ -0,0 +1,2 @@ +* LIST (\Noselect) "/" "" +A######## OK LIST Completed diff --git a/UnitTests/Net/Imap/Resources/common/list-nil-folder-delim.txt b/UnitTests/Net/Imap/Resources/common/list-nil-folder-delim.txt new file mode 100644 index 0000000000..2b09dbecb7 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/common/list-nil-folder-delim.txt @@ -0,0 +1,4 @@ +* LIST () "/" INBOX +* LIST (\HasNoChildren) NIL "Folder1" +* LIST (\HasNoChildren) "" "Folder2" +A######## OK LIST Completed diff --git a/UnitTests/Net/Imap/Resources/common/namespace.txt b/UnitTests/Net/Imap/Resources/common/namespace.txt new file mode 100644 index 0000000000..a3a9aff335 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/common/namespace.txt @@ -0,0 +1,2 @@ +* NAMESPACE (("" "/")) (("Other Users/" "/" "TRANSLATION" ("Andere Ben&APw-tzer/"))) (("Public Folders/" "/" "TRANSLATION" ("Gemeinsame Postf&AM8-cher/"))) +A######## OK Success diff --git a/UnitTests/Net/Imap/Resources/common/preauth-capability-greeting.txt b/UnitTests/Net/Imap/Resources/common/preauth-capability-greeting.txt new file mode 100644 index 0000000000..80872cf174 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/common/preauth-capability-greeting.txt @@ -0,0 +1 @@ +* PREAUTH [CAPABILITY IMAP4rev1] diff --git a/UnitTests/Net/Imap/Resources/common/preauth-greeting.txt b/UnitTests/Net/Imap/Resources/common/preauth-greeting.txt new file mode 100644 index 0000000000..e6557c5ea9 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/common/preauth-greeting.txt @@ -0,0 +1 @@ +* PREAUTH IMAP4rev1 server logged in as Smith diff --git a/UnitTests/Net/Imap/Resources/common/select-inbox-annotate-no-modseq.txt b/UnitTests/Net/Imap/Resources/common/select-inbox-annotate-no-modseq.txt new file mode 100644 index 0000000000..6ff1e34227 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/common/select-inbox-annotate-no-modseq.txt @@ -0,0 +1,9 @@ +* FLAGS (\Answered \Flagged \Deleted \Seen \Draft) +* OK [PERMANENTFLAGS (\Answered \Flagged \Deleted \Seen \Draft \*)] Flags permitted. +* 8 EXISTS +* 8 RECENT +* OK [UNSEEN 1] First unseen. +* OK [UIDVALIDITY 1436832084] UIDs valid +* OK [UIDNEXT 9] Predicted next UID +* OK [ANNOTATIONS 20480 NOPRIVATE] +A######## OK [READ-WRITE] Select completed (0.000 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/common/select-inbox-annotate-none.txt b/UnitTests/Net/Imap/Resources/common/select-inbox-annotate-none.txt new file mode 100644 index 0000000000..0b5e824a30 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/common/select-inbox-annotate-none.txt @@ -0,0 +1,10 @@ +* FLAGS (\Answered \Flagged \Deleted \Seen \Draft) +* OK [PERMANENTFLAGS (\Answered \Flagged \Deleted \Seen \Draft \*)] Flags permitted. +* 8 EXISTS +* 8 RECENT +* OK [UNSEEN 1] First unseen. +* OK [UIDVALIDITY 1436832084] UIDs valid +* OK [UIDNEXT 9] Predicted next UID +* OK [HIGHESTMODSEQ 2] Highest +* OK [ANNOTATIONS NONE] +A######## OK [READ-WRITE] Select completed (0.000 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/common/select-inbox-annotate-readonly.txt b/UnitTests/Net/Imap/Resources/common/select-inbox-annotate-readonly.txt new file mode 100644 index 0000000000..a470ab516d --- /dev/null +++ b/UnitTests/Net/Imap/Resources/common/select-inbox-annotate-readonly.txt @@ -0,0 +1,10 @@ +* FLAGS (\Answered \Flagged \Deleted \Seen \Draft) +* OK [PERMANENTFLAGS (\Answered \Flagged \Deleted \Seen \Draft \*)] Flags permitted. +* 8 EXISTS +* 8 RECENT +* OK [UNSEEN 1] First unseen. +* OK [UIDVALIDITY 1436832084] UIDs valid +* OK [UIDNEXT 9] Predicted next UID +* OK [HIGHESTMODSEQ 2] Highest +* OK [ANNOTATIONS READ-ONLY] +A######## OK [READ-WRITE] Select completed (0.000 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/common/select-inbox-annotate.txt b/UnitTests/Net/Imap/Resources/common/select-inbox-annotate.txt new file mode 100644 index 0000000000..3388b8b7cf --- /dev/null +++ b/UnitTests/Net/Imap/Resources/common/select-inbox-annotate.txt @@ -0,0 +1,10 @@ +* FLAGS (\Answered \Flagged \Deleted \Seen \Draft) +* OK [PERMANENTFLAGS (\Answered \Flagged \Deleted \Seen \Draft \*)] Flags permitted. +* 8 EXISTS +* 8 RECENT +* OK [UNSEEN 1] First unseen. +* OK [UIDVALIDITY 1436832084] UIDs valid +* OK [UIDNEXT 9] Predicted next UID +* OK [HIGHESTMODSEQ 2] Highest +* OK [ANNOTATIONS 20480 NOPRIVATE] +A######## OK [READ-WRITE] Select completed (0.000 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/common/select-inbox-no-modseq.txt b/UnitTests/Net/Imap/Resources/common/select-inbox-no-modseq.txt new file mode 100644 index 0000000000..2c0abe713d --- /dev/null +++ b/UnitTests/Net/Imap/Resources/common/select-inbox-no-modseq.txt @@ -0,0 +1,8 @@ +* FLAGS (\Answered \Flagged \Deleted \Seen \Draft) +* OK [PERMANENTFLAGS (\Answered \Flagged \Deleted \Seen \Draft \*)] Flags permitted. +* 8 EXISTS +* 8 RECENT +* OK [UNSEEN 1] First unseen. +* OK [UIDVALIDITY 1436832084] UIDs valid +* OK [UIDNEXT 9] Predicted next UID +A######## OK [READ-WRITE] Select completed (0.000 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/common/select-inbox.txt b/UnitTests/Net/Imap/Resources/common/select-inbox.txt index 44f9399a5d..f5468593ba 100644 --- a/UnitTests/Net/Imap/Resources/common/select-inbox.txt +++ b/UnitTests/Net/Imap/Resources/common/select-inbox.txt @@ -6,4 +6,4 @@ * OK [UIDVALIDITY 1436832084] UIDs valid * OK [UIDNEXT 9] Predicted next UID * OK [HIGHESTMODSEQ 2] Highest -A00000004 OK [READ-WRITE] Select completed (0.000 + 0.000 secs). +A######## OK [READ-WRITE] Select completed (0.000 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/common/setquota.txt b/UnitTests/Net/Imap/Resources/common/setquota.txt index 9e58bc57f9..e8e4be4a0f 100644 --- a/UnitTests/Net/Imap/Resources/common/setquota.txt +++ b/UnitTests/Net/Imap/Resources/common/setquota.txt @@ -1,2 +1,2 @@ * QUOTA "" (MESSAGE 1107 1000000 STORAGE 3783 5242880) -A00000007 OK Success +A######## OK Success diff --git a/UnitTests/Net/Imap/Resources/common/status-literal-folder.txt b/UnitTests/Net/Imap/Resources/common/status-literal-folder.txt new file mode 100644 index 0000000000..de3411f256 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/common/status-literal-folder.txt @@ -0,0 +1,3 @@ +* STATUS {19} +Literal Folder Name (MESSAGES 60) +A######## OK STATUS Completed diff --git a/UnitTests/Net/Imap/Resources/courier/capability.txt b/UnitTests/Net/Imap/Resources/courier/capability.txt new file mode 100644 index 0000000000..0a7e89dd94 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/courier/capability.txt @@ -0,0 +1,2 @@ +* CAPABILITY IMAP4rev1 UIDPLUS CHILDREN NAMESPACE THREAD=ORDEREDSUBJECT THREAD=REFERENCES SORT QUOTA IDLE AUTH=PLAIN ACL ACL2=UNION +A######## OK CAPABILITY completed diff --git a/UnitTests/Net/Imap/Resources/courier/greeting.txt b/UnitTests/Net/Imap/Resources/courier/greeting.txt new file mode 100644 index 0000000000..f982806f13 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/courier/greeting.txt @@ -0,0 +1 @@ +* OK [CAPABILITY IMAP4rev1 UIDPLUS CHILDREN NAMESPACE THREAD=ORDEREDSUBJECT THREAD=REFERENCES SORT QUOTA IDLE ACL ACL2=UNION STARTTLS] Courier-IMAP ready. Copyright 1998-2011 Double Precision, Inc. See COPYING for distribution information. diff --git a/UnitTests/Net/Imap/Resources/cyrus/authenticate.txt b/UnitTests/Net/Imap/Resources/cyrus/authenticate.txt new file mode 100644 index 0000000000..44112bf29f --- /dev/null +++ b/UnitTests/Net/Imap/Resources/cyrus/authenticate.txt @@ -0,0 +1 @@ +A######## OK [CAPABILITY IMAP4 IMAP4rev1 LITERAL+ ID LOGINDISABLED ACL RIGHTS=kxte QUOTA MAILBOX-REFERRALS NAMESPACE UIDPLUS NO_ATOMIC_RENAME UNSELECT CHILDREN MULTIAPPEND BINARY SORT SORT=MODSEQ THREAD=ORDEREDSUBJECT THREAD=REFERENCES ANNOTATEMORE CATENATE CONDSTORE SCAN IDLE LISTEXT LIST-SUBSCRIBED X-NETSCAPE URLAUTH] Success (no protection) diff --git a/UnitTests/Net/Imap/Resources/cyrus/capability.txt b/UnitTests/Net/Imap/Resources/cyrus/capability.txt new file mode 100644 index 0000000000..21fe00de96 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/cyrus/capability.txt @@ -0,0 +1,2 @@ +* CAPABILITY IMAP4 IMAP4rev1 LITERAL+ ID STARTTLS AUTH=PLAIN SASL-IR ACL RIGHTS=kxte QUOTA MAILBOX-REFERRALS NAMESPACE UIDPLUS NO_ATOMIC_RENAME UNSELECT CHILDREN MULTIAPPEND BINARY SORT SORT=MODSEQ THREAD=ORDEREDSUBJECT THREAD=REFERENCES ANNOTATEMORE CATENATE CONDSTORE SCAN IDLE LISTEXT LIST-SUBSCRIBED X-NETSCAPE URLAUTH +A######## OK CAPABILITY completed diff --git a/UnitTests/Net/Imap/Resources/cyrus/greeting.txt b/UnitTests/Net/Imap/Resources/cyrus/greeting.txt new file mode 100644 index 0000000000..d0151d82eb --- /dev/null +++ b/UnitTests/Net/Imap/Resources/cyrus/greeting.txt @@ -0,0 +1 @@ +* OK [CAPABILITY IMAP4 IMAP4rev1 LITERAL+ ID STARTTLS AUTH=PLAIN SASL-IR] imap.ecs.soton.ac.uk Cyrus IMAP v2.3.13-Invoca-RPM-2.3.13-1.0.JKF server ready diff --git a/UnitTests/Net/Imap/Resources/domino/capability.txt b/UnitTests/Net/Imap/Resources/domino/capability.txt new file mode 100644 index 0000000000..5caa8ab10b --- /dev/null +++ b/UnitTests/Net/Imap/Resources/domino/capability.txt @@ -0,0 +1,2 @@ +* CAPABILITY IMAP4rev1 AUTH=PLAIN LITERAL+ NAMESPACE QUOTA UIDPLUS +A######## OK CAPABILITY completed diff --git a/UnitTests/Net/Imap/Resources/domino/fetch-extra-parens.txt b/UnitTests/Net/Imap/Resources/domino/fetch-extra-parens.txt new file mode 100644 index 0000000000..5065e6ae55 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/domino/fetch-extra-parens.txt @@ -0,0 +1,30 @@ +* 2 FETCH (UID 14935 (ENVELOPE (NIL NIL NIL NIL NIL NIL NIL NIL NIL NIL)) BODYSTRUCTURE ("TEXT" "PLAIN" NIL NIL NIL "7BIT" 0 0 NIL)) +* 4 FETCH (UID 14937 (ENVELOPE (NIL NIL NIL NIL NIL NIL NIL NIL NIL NIL)) BODYSTRUCTURE ("TEXT" "PLAIN" NIL NIL NIL "7BIT" 0 0 NIL)) +* 3 FETCH (UID 14936 (ENVELOPE (NIL NIL NIL NIL NIL NIL NIL NIL NIL NIL)) BODYSTRUCTURE ("TEXT" "PLAIN" NIL NIL NIL "7BIT" 0 0 NIL)) +* 1 FETCH (UID 14934 (ENVELOPE (NIL NIL NIL NIL NIL NIL NIL NIL NIL NIL)) BODYSTRUCTURE ("TEXT" "PLAIN" NIL NIL NIL "7BIT" 0 0 NIL)) +* 6 FETCH (UID 14939 (ENVELOPE (NIL NIL NIL NIL NIL NIL NIL NIL NIL NIL)) BODYSTRUCTURE ("TEXT" "PLAIN" NIL NIL NIL "7BIT" 0 0 NIL)) +* 5 FETCH (UID 14938 (ENVELOPE (NIL NIL NIL NIL NIL NIL NIL NIL NIL NIL)) BODYSTRUCTURE ("TEXT" "PLAIN" NIL NIL NIL "7BIT" 0 0 NIL)) +* 8 FETCH (UID 14941 (ENVELOPE (NIL NIL NIL NIL NIL NIL NIL NIL NIL NIL)) BODYSTRUCTURE ("TEXT" "PLAIN" NIL NIL NIL "7BIT" 0 0 NIL)) +* 7 FETCH (UID 14940 (ENVELOPE (NIL NIL NIL NIL NIL NIL NIL NIL NIL NIL)) BODYSTRUCTURE ("TEXT" "PLAIN" NIL NIL NIL "7BIT" 0 0 NIL)) +* 10 FETCH (UID 14943 (ENVELOPE (NIL NIL NIL NIL NIL NIL NIL NIL NIL NIL)) BODYSTRUCTURE ("TEXT" "PLAIN" NIL NIL NIL "7BIT" 0 0 NIL)) +* 9 FETCH (UID 14942 (ENVELOPE (NIL NIL NIL NIL NIL NIL NIL NIL NIL NIL)) BODYSTRUCTURE ("TEXT" "PLAIN" NIL NIL NIL "7BIT" 0 0 NIL)) +* 14 FETCH (UID 14947 (ENVELOPE (NIL NIL NIL NIL NIL NIL NIL NIL NIL NIL)) BODYSTRUCTURE ("TEXT" "PLAIN" NIL NIL NIL "7BIT" 0 0 NIL)) +* 13 FETCH (UID 14946 (ENVELOPE (NIL NIL NIL NIL NIL NIL NIL NIL NIL NIL)) BODYSTRUCTURE ("TEXT" "PLAIN" NIL NIL NIL "7BIT" 0 0 NIL)) +* 15 FETCH (UID 14948 (ENVELOPE (NIL NIL NIL NIL NIL NIL NIL NIL NIL NIL)) BODYSTRUCTURE ("TEXT" "PLAIN" NIL NIL NIL "7BIT" 0 0 NIL)) +* 11 FETCH (UID 14944 (ENVELOPE (NIL NIL NIL NIL NIL NIL NIL NIL NIL NIL)) BODYSTRUCTURE ("TEXT" "PLAIN" NIL NIL NIL "7BIT" 0 0 NIL)) +* 12 FETCH (UID 14945 (ENVELOPE (NIL NIL NIL NIL NIL NIL NIL NIL NIL NIL)) BODYSTRUCTURE ("TEXT" "PLAIN" NIL NIL NIL "7BIT" 0 0 NIL)) +* 17 FETCH (UID 14950 (ENVELOPE (NIL NIL NIL NIL NIL NIL NIL NIL NIL NIL)) BODYSTRUCTURE ("TEXT" "PLAIN" NIL NIL NIL "7BIT" 0 0 NIL)) +* 16 FETCH (UID 14949 (ENVELOPE (NIL NIL NIL NIL NIL NIL NIL NIL NIL NIL)) BODYSTRUCTURE ("TEXT" "PLAIN" NIL NIL NIL "7BIT" 0 0 NIL)) +* 19 FETCH (UID 14952 (ENVELOPE (NIL NIL NIL NIL NIL NIL NIL NIL NIL NIL)) BODYSTRUCTURE ("TEXT" "PLAIN" NIL NIL NIL "7BIT" 0 0 NIL)) +* 18 FETCH (UID 14951 (ENVELOPE (NIL NIL NIL NIL NIL NIL NIL NIL NIL NIL)) BODYSTRUCTURE ("TEXT" "PLAIN" NIL NIL NIL "7BIT" 0 0 NIL)) +* 20 FETCH (UID 14953 (ENVELOPE (NIL NIL NIL NIL NIL NIL NIL NIL NIL NIL)) BODYSTRUCTURE ("TEXT" "PLAIN" NIL NIL NIL "7BIT" 0 0 NIL)) +* 21 FETCH (UID 14954 (ENVELOPE (NIL NIL NIL NIL NIL NIL NIL NIL NIL NIL)) BODYSTRUCTURE ("TEXT" "PLAIN" NIL NIL NIL "7BIT" 0 0 NIL)) +* 24 FETCH (UID 14957 (ENVELOPE (NIL NIL NIL NIL NIL NIL NIL NIL NIL NIL)) BODYSTRUCTURE ("TEXT" "PLAIN" NIL NIL NIL "7BIT" 0 0 NIL)) +* 23 FETCH (UID 14956 (ENVELOPE (NIL NIL NIL NIL NIL NIL NIL NIL NIL NIL)) BODYSTRUCTURE ("TEXT" "PLAIN" NIL NIL NIL "7BIT" 0 0 NIL)) +* 22 FETCH (UID 14955 (ENVELOPE (NIL NIL NIL NIL NIL NIL NIL NIL NIL NIL)) BODYSTRUCTURE ("TEXT" "PLAIN" NIL NIL NIL "7BIT" 0 0 NIL)) +* 25 FETCH (UID 14958 (ENVELOPE (NIL NIL NIL NIL NIL NIL NIL NIL NIL NIL)) BODYSTRUCTURE ("TEXT" "PLAIN" NIL NIL NIL "7BIT" 0 0 NIL)) +* 26 FETCH (UID 14959 (ENVELOPE (NIL NIL NIL NIL NIL NIL NIL NIL NIL NIL)) BODYSTRUCTURE ("TEXT" "PLAIN" NIL NIL NIL "7BIT" 0 0 NIL)) +* 28 FETCH (UID 14961 (ENVELOPE (NIL NIL NIL NIL NIL NIL NIL NIL NIL NIL)) BODYSTRUCTURE ("TEXT" "PLAIN" NIL NIL NIL "7BIT" 0 0 NIL)) +* 27 FETCH (UID 14960 (ENVELOPE (NIL NIL NIL NIL NIL NIL NIL NIL NIL NIL)) BODYSTRUCTURE ("TEXT" "PLAIN" NIL NIL NIL "7BIT" 0 0 NIL)) +* 29 FETCH (UID 14962 (ENVELOPE (NIL NIL NIL NIL NIL NIL NIL NIL NIL NIL)) BODYSTRUCTURE ("TEXT" "PLAIN" NIL NIL NIL "7BIT" 0 0 NIL)) +A######## OK FETCH completed diff --git a/UnitTests/Net/Imap/Resources/domino/greeting.txt b/UnitTests/Net/Imap/Resources/domino/greeting.txt new file mode 100644 index 0000000000..c7658614e8 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/domino/greeting.txt @@ -0,0 +1 @@ +* OK Domino IMAP4 Server Release 9.0.1FP9 ready Fri, 14 Dec 2018 08:38:25 +0100 diff --git a/UnitTests/Net/Imap/Resources/domino/list-inbox.txt b/UnitTests/Net/Imap/Resources/domino/list-inbox.txt new file mode 100644 index 0000000000..527e9dbe3d --- /dev/null +++ b/UnitTests/Net/Imap/Resources/domino/list-inbox.txt @@ -0,0 +1,2 @@ +* LIST (\Noinferiors \HasNoChildren) "\\" Inbox +A######## OK LIST completed diff --git a/UnitTests/Net/Imap/Resources/domino/namespace.txt b/UnitTests/Net/Imap/Resources/domino/namespace.txt new file mode 100644 index 0000000000..d6155d242d --- /dev/null +++ b/UnitTests/Net/Imap/Resources/domino/namespace.txt @@ -0,0 +1,2 @@ +* NAMESPACE (("" "\\")) (("Other\\" "\\")) (("Shared\\" "\\")) +A######## OK NAMESPACE completed diff --git a/UnitTests/Net/Imap/Resources/dovecot/append.1.txt b/UnitTests/Net/Imap/Resources/dovecot/append.1.txt new file mode 100644 index 0000000000..f16a326814 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/dovecot/append.1.txt @@ -0,0 +1 @@ +A######## OK [APPENDUID 1436832084 1] Append completed (0.000 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/append.2.txt b/UnitTests/Net/Imap/Resources/dovecot/append.2.txt new file mode 100644 index 0000000000..a16bd0aeca --- /dev/null +++ b/UnitTests/Net/Imap/Resources/dovecot/append.2.txt @@ -0,0 +1 @@ +A######## OK [APPENDUID 1436832084 2] Append completed (0.000 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/append.3.txt b/UnitTests/Net/Imap/Resources/dovecot/append.3.txt new file mode 100644 index 0000000000..653454b00c --- /dev/null +++ b/UnitTests/Net/Imap/Resources/dovecot/append.3.txt @@ -0,0 +1 @@ +A######## OK [APPENDUID 1436832084 3] Append completed (0.000 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/append.4.txt b/UnitTests/Net/Imap/Resources/dovecot/append.4.txt new file mode 100644 index 0000000000..d72863c295 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/dovecot/append.4.txt @@ -0,0 +1 @@ +A######## OK [APPENDUID 1436832084 4] Append completed (0.000 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/append.5.txt b/UnitTests/Net/Imap/Resources/dovecot/append.5.txt new file mode 100644 index 0000000000..14e7269711 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/dovecot/append.5.txt @@ -0,0 +1 @@ +A######## OK [APPENDUID 1436832084 5] Append completed (0.000 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/append.6.txt b/UnitTests/Net/Imap/Resources/dovecot/append.6.txt new file mode 100644 index 0000000000..68399a6278 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/dovecot/append.6.txt @@ -0,0 +1 @@ +A######## OK [APPENDUID 1436832084 6] Append completed (0.000 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/append.7.txt b/UnitTests/Net/Imap/Resources/dovecot/append.7.txt new file mode 100644 index 0000000000..6ee988084a --- /dev/null +++ b/UnitTests/Net/Imap/Resources/dovecot/append.7.txt @@ -0,0 +1 @@ +A######## OK [APPENDUID 1436832084 7] Append completed (0.000 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/append.8.txt b/UnitTests/Net/Imap/Resources/dovecot/append.8.txt new file mode 100644 index 0000000000..a631ab76c8 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/dovecot/append.8.txt @@ -0,0 +1 @@ +A######## OK [APPENDUID 1436832084 8] Append completed (0.000 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/authenticate+annotate+replace.txt b/UnitTests/Net/Imap/Resources/dovecot/authenticate+annotate+replace.txt new file mode 100644 index 0000000000..19dc6f9aa8 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/dovecot/authenticate+annotate+replace.txt @@ -0,0 +1 @@ +A######## OK [CAPABILITY IMAP4rev1 LITERAL+ SASL-IR LOGIN-REFERRALS ID ENABLE IDLE SORT SORT=DISPLAY THREAD=REFERENCES THREAD=REFS THREAD=ORDEREDSUBJECT MULTIAPPEND URL-PARTIAL CATENATE UNSELECT CHILDREN NAMESPACE UIDPLUS LIST-EXTENDED I18NLEVEL=1 CONDSTORE QRESYNC ESEARCH ESORT SEARCHRES WITHIN CONTEXT=SEARCH LIST-STATUS BINARY MOVE SPECIAL-USE ANNOTATE-EXPERIMENT-1 REPLACE] Logged in diff --git a/UnitTests/Net/Imap/Resources/dovecot/authenticate+annotate.txt b/UnitTests/Net/Imap/Resources/dovecot/authenticate+annotate.txt new file mode 100644 index 0000000000..56f668bbc1 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/dovecot/authenticate+annotate.txt @@ -0,0 +1 @@ +A######## OK [CAPABILITY IMAP4rev1 LITERAL+ SASL-IR LOGIN-REFERRALS ID ENABLE IDLE SORT SORT=DISPLAY THREAD=REFERENCES THREAD=REFS THREAD=ORDEREDSUBJECT MULTIAPPEND URL-PARTIAL CATENATE UNSELECT CHILDREN NAMESPACE UIDPLUS LIST-EXTENDED I18NLEVEL=1 CONDSTORE QRESYNC ESEARCH ESORT SEARCHRES WITHIN CONTEXT=SEARCH LIST-STATUS BINARY MOVE SPECIAL-USE ANNOTATE-EXPERIMENT-1] Logged in diff --git a/UnitTests/Net/Imap/Resources/dovecot/authenticate+filters.txt b/UnitTests/Net/Imap/Resources/dovecot/authenticate+filters.txt new file mode 100644 index 0000000000..4246cf926a --- /dev/null +++ b/UnitTests/Net/Imap/Resources/dovecot/authenticate+filters.txt @@ -0,0 +1 @@ +A######## OK [CAPABILITY IMAP4rev1 LITERAL+ SASL-IR LOGIN-REFERRALS ID ENABLE IDLE SORT SORT=DISPLAY THREAD=REFERENCES THREAD=REFS THREAD=ORDEREDSUBJECT MULTIAPPEND URL-PARTIAL CATENATE UNSELECT CHILDREN NAMESPACE UIDPLUS LIST-EXTENDED I18NLEVEL=1 CONDSTORE QRESYNC ESEARCH ESORT SEARCHRES WITHIN CONTEXT=SEARCH LIST-STATUS BINARY MOVE SPECIAL-USE FILTERS] Logged in diff --git a/UnitTests/Net/Imap/Resources/dovecot/authenticate+fuzzy.txt b/UnitTests/Net/Imap/Resources/dovecot/authenticate+fuzzy.txt new file mode 100644 index 0000000000..b8052335bf --- /dev/null +++ b/UnitTests/Net/Imap/Resources/dovecot/authenticate+fuzzy.txt @@ -0,0 +1 @@ +A######## OK [CAPABILITY IMAP4rev1 LITERAL+ SASL-IR LOGIN-REFERRALS ID ENABLE IDLE SORT SORT=DISPLAY THREAD=REFERENCES THREAD=REFS THREAD=ORDEREDSUBJECT MULTIAPPEND URL-PARTIAL CATENATE UNSELECT CHILDREN NAMESPACE UIDPLUS LIST-EXTENDED I18NLEVEL=1 CONDSTORE QRESYNC ESEARCH ESORT SEARCHRES WITHIN CONTEXT=SEARCH LIST-STATUS BINARY MOVE SPECIAL-USE SEARCH=FUZZY] Logged in diff --git a/UnitTests/Net/Imap/Resources/dovecot/authenticate+gmail-capabilities.txt b/UnitTests/Net/Imap/Resources/dovecot/authenticate+gmail-capabilities.txt new file mode 100644 index 0000000000..a2a8b5951d --- /dev/null +++ b/UnitTests/Net/Imap/Resources/dovecot/authenticate+gmail-capabilities.txt @@ -0,0 +1 @@ +A######## OK [CAPABILITY IMAP4rev1 LITERAL+ SASL-IR LOGIN-REFERRALS ID ENABLE IDLE SORT SORT=DISPLAY THREAD=REFERENCES THREAD=REFS THREAD=ORDEREDSUBJECT MULTIAPPEND URL-PARTIAL CATENATE UNSELECT CHILDREN NAMESPACE UIDPLUS LIST-EXTENDED I18NLEVEL=1 CONDSTORE QRESYNC ESEARCH ESORT SEARCHRES WITHIN CONTEXT=SEARCH LIST-STATUS BINARY MOVE SPECIAL-USE X-GM-EXT-1] Logged in diff --git a/UnitTests/Net/Imap/Resources/dovecot/authenticate+replace.txt b/UnitTests/Net/Imap/Resources/dovecot/authenticate+replace.txt new file mode 100644 index 0000000000..13262a0c62 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/dovecot/authenticate+replace.txt @@ -0,0 +1 @@ +A######## OK [CAPABILITY IMAP4rev1 LITERAL+ SASL-IR LOGIN-REFERRALS ID ENABLE IDLE SORT SORT=DISPLAY THREAD=REFERENCES THREAD=REFS THREAD=ORDEREDSUBJECT MULTIAPPEND URL-PARTIAL CATENATE UNSELECT CHILDREN NAMESPACE UIDPLUS LIST-EXTENDED I18NLEVEL=1 CONDSTORE QRESYNC ESEARCH ESORT SEARCHRES WITHIN CONTEXT=SEARCH LIST-STATUS BINARY MOVE SPECIAL-USE REPLACE] Logged in diff --git a/UnitTests/Net/Imap/Resources/dovecot/authenticate+savedate.txt b/UnitTests/Net/Imap/Resources/dovecot/authenticate+savedate.txt new file mode 100644 index 0000000000..f8db1e5585 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/dovecot/authenticate+savedate.txt @@ -0,0 +1 @@ +A######## OK [CAPABILITY IMAP4rev1 LITERAL+ SASL-IR LOGIN-REFERRALS ID ENABLE IDLE SORT SORT=DISPLAY THREAD=REFERENCES THREAD=REFS THREAD=ORDEREDSUBJECT MULTIAPPEND URL-PARTIAL CATENATE UNSELECT CHILDREN NAMESPACE UIDPLUS LIST-EXTENDED I18NLEVEL=1 CONDSTORE QRESYNC ESEARCH ESORT SEARCHRES WITHIN CONTEXT=SEARCH LIST-STATUS BINARY MOVE SPECIAL-USE SAVEDATE] Logged in diff --git a/UnitTests/Net/Imap/Resources/dovecot/authenticate.txt b/UnitTests/Net/Imap/Resources/dovecot/authenticate.txt index 9cc8cb52ef..f4c93eb463 100644 --- a/UnitTests/Net/Imap/Resources/dovecot/authenticate.txt +++ b/UnitTests/Net/Imap/Resources/dovecot/authenticate.txt @@ -1 +1 @@ -A00000000 OK [CAPABILITY IMAP4rev1 LITERAL+ SASL-IR LOGIN-REFERRALS ID ENABLE IDLE SORT SORT=DISPLAY THREAD=REFERENCES THREAD=REFS THREAD=ORDEREDSUBJECT MULTIAPPEND URL-PARTIAL CATENATE UNSELECT CHILDREN NAMESPACE UIDPLUS LIST-EXTENDED I18NLEVEL=1 CONDSTORE QRESYNC ESEARCH ESORT SEARCHRES WITHIN CONTEXT=SEARCH LIST-STATUS BINARY MOVE SPECIAL-USE] Logged in +A######## OK [CAPABILITY IMAP4rev1 LITERAL+ SASL-IR LOGIN-REFERRALS ID ENABLE IDLE SORT SORT=DISPLAY THREAD=REFERENCES THREAD=REFS THREAD=ORDEREDSUBJECT MULTIAPPEND URL-PARTIAL CATENATE UNSELECT CHILDREN NAMESPACE UIDPLUS LIST-EXTENDED I18NLEVEL=1 CONDSTORE QRESYNC ESEARCH ESORT SEARCHRES WITHIN CONTEXT=SEARCH LIST-STATUS BINARY MOVE SPECIAL-USE] Logged in diff --git a/UnitTests/Net/Imap/Resources/dovecot/copy.txt b/UnitTests/Net/Imap/Resources/dovecot/copy.txt index 32a958b339..40444ed604 100644 --- a/UnitTests/Net/Imap/Resources/dovecot/copy.txt +++ b/UnitTests/Net/Imap/Resources/dovecot/copy.txt @@ -1 +1 @@ -A00000027 OK [COPYUID 1436832101 1:7 1:7] Copy completed (0.020 + 0.000 secs). +A######## OK [COPYUID 1436832101 1:7 1:7] Copy completed (0.020 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/enable-qresync.txt b/UnitTests/Net/Imap/Resources/dovecot/enable-qresync.txt index 7626f38302..840b6805bd 100644 --- a/UnitTests/Net/Imap/Resources/dovecot/enable-qresync.txt +++ b/UnitTests/Net/Imap/Resources/dovecot/enable-qresync.txt @@ -1,2 +1,2 @@ * ENABLED QRESYNC CONDSTORE -A00000004 OK Enabled (0.000 + 0.000 secs). +A######## OK Enabled (0.000 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/examine-folder.txt b/UnitTests/Net/Imap/Resources/dovecot/examine-folder.txt new file mode 100644 index 0000000000..25d67a5434 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/dovecot/examine-folder.txt @@ -0,0 +1,8 @@ +* FLAGS (\Answered \Flagged \Deleted \Seen \Draft) +* OK [PERMANENTFLAGS ()] Read-only mailbox. +* 0 EXISTS +* 0 RECENT +* OK [UIDVALIDITY 1543354378] UIDs valid +* OK [UIDNEXT 1] Predicted next UID +* OK [HIGHESTMODSEQ 1] Highest +A######## OK [READ-ONLY] Examine completed (0.000 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/expunge.txt b/UnitTests/Net/Imap/Resources/dovecot/expunge.txt index 1be21000e1..ea478880ca 100644 --- a/UnitTests/Net/Imap/Resources/dovecot/expunge.txt +++ b/UnitTests/Net/Imap/Resources/dovecot/expunge.txt @@ -1,3 +1,3 @@ * VANISHED 1:14 * 0 RECENT -A00000062 OK [HIGHESTMODSEQ 7] Expunge completed. +A######## OK [HIGHESTMODSEQ 7] Expunge completed. diff --git a/UnitTests/Net/Imap/Resources/dovecot/fetch1.txt b/UnitTests/Net/Imap/Resources/dovecot/fetch1.txt index 85b60045a9..13c9341535 100644 --- a/UnitTests/Net/Imap/Resources/dovecot/fetch1.txt +++ b/UnitTests/Net/Imap/Resources/dovecot/fetch1.txt @@ -1,8 +1,9 @@ -* 1 FETCH (UID 1 FLAGS (\Answered \Seen \Draft) MODSEQ (4)) +* 1 FETCH (UID 1 FLAGS (\Answered \Seen \Draft) MODSEQ (4) XAOL.SPAM.REASON 0 XAOL-MSGID 1 XAOL-PAREN-LIST (XAOL-TOKEN-PARAM1 XAOL-TOKEN-VALUE1 XAOL-TOKEN-PARAM2 {17} +XAOL-TOKEN-VALUE2 XAOL-TOKEN-PARAM3 (XAOL-SUBTOKEN-PARAM XAOL-SUBTOKEN-VALUE))) * 2 FETCH (UID 2 FLAGS (\Answered \Seen \Draft) MODSEQ (4)) * 3 FETCH (UID 3 FLAGS (\Answered \Seen \Draft) MODSEQ (4)) * 4 FETCH (UID 4 FLAGS (\Seen \Draft) MODSEQ (3)) * 5 FETCH (UID 5 FLAGS (\Seen \Draft) MODSEQ (3)) * 6 FETCH (UID 6 FLAGS (\Seen \Draft) MODSEQ (3)) * 7 FETCH (UID 7 FLAGS (\Seen \Draft) MODSEQ (3)) -A00000021 OK Fetch completed (0.001 + 0.000 secs). +A######## OK Fetch completed (0.001 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/fetch10.txt b/UnitTests/Net/Imap/Resources/dovecot/fetch10.txt deleted file mode 100644 index b93a91dc7b..0000000000 --- a/UnitTests/Net/Imap/Resources/dovecot/fetch10.txt +++ /dev/null @@ -1,53 +0,0 @@ -* 1 FETCH (UID 1 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:28:25 -0400" RFC822.SIZE 253 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:28:25 -0400" "A" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {2} - -) -* 2 FETCH (UID 2 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:29:25 -0400" RFC822.SIZE 282 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:29:25 -0400" "B" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {31} -References: - -) -* 3 FETCH (UID 3 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:30:25 -0400" RFC822.SIZE 298 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:30:25 -0400" "C" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {47} -References: - -) -* 4 FETCH (UID 4 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:31:25 -0400" RFC822.SIZE 282 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:31:25 -0400" "D" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {31} -References: - -) -* 5 FETCH (UID 5 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:32:25 -0400" RFC822.SIZE 330 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:32:25 -0400" "E" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {79} -References: - -) -* 6 FETCH (UID 6 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:33:25 -0400" RFC822.SIZE 282 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:33:25 -0400" "F" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {31} -References: - -) -* 7 FETCH (UID 7 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:34:25 -0400" RFC822.SIZE 253 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:34:25 -0400" "G" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {2} - -) -* 8 FETCH (UID 8 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:28:25 -0400" RFC822.SIZE 253 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:28:25 -0400" "A" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {2} - -) -* 9 FETCH (UID 9 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:29:25 -0400" RFC822.SIZE 282 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:29:25 -0400" "B" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {31} -References: - -) -* 10 FETCH (UID 10 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:30:25 -0400" RFC822.SIZE 298 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:30:25 -0400" "C" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {47} -References: - -) -* 11 FETCH (UID 11 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:31:25 -0400" RFC822.SIZE 282 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:31:25 -0400" "D" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {31} -References: - -) -* 12 FETCH (UID 12 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:32:25 -0400" RFC822.SIZE 330 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:32:25 -0400" "E" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {79} -References: - -) -* 13 FETCH (UID 13 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:33:25 -0400" RFC822.SIZE 282 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:33:25 -0400" "F" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {31} -References: - -) -* 14 FETCH (UID 14 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:34:25 -0400" RFC822.SIZE 253 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:34:25 -0400" "G" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {2} - -) -A00000038 OK Fetch completed (0.019 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/fetch11.txt b/UnitTests/Net/Imap/Resources/dovecot/fetch11.txt deleted file mode 100644 index 41868ca06b..0000000000 --- a/UnitTests/Net/Imap/Resources/dovecot/fetch11.txt +++ /dev/null @@ -1,53 +0,0 @@ -* 1 FETCH (UID 1 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:28:25 -0400" RFC822.SIZE 253 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:28:25 -0400" "A" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {2} - -) -* 2 FETCH (UID 2 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:29:25 -0400" RFC822.SIZE 282 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:29:25 -0400" "B" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {31} -References: - -) -* 3 FETCH (UID 3 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:30:25 -0400" RFC822.SIZE 298 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:30:25 -0400" "C" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {47} -References: - -) -* 4 FETCH (UID 4 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:31:25 -0400" RFC822.SIZE 282 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:31:25 -0400" "D" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {31} -References: - -) -* 5 FETCH (UID 5 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:32:25 -0400" RFC822.SIZE 330 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:32:25 -0400" "E" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {79} -References: - -) -* 6 FETCH (UID 6 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:33:25 -0400" RFC822.SIZE 282 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:33:25 -0400" "F" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {31} -References: - -) -* 7 FETCH (UID 7 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:34:25 -0400" RFC822.SIZE 253 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:34:25 -0400" "G" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {2} - -) -* 8 FETCH (UID 8 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:28:25 -0400" RFC822.SIZE 253 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:28:25 -0400" "A" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {2} - -) -* 9 FETCH (UID 9 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:29:25 -0400" RFC822.SIZE 282 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:29:25 -0400" "B" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {31} -References: - -) -* 10 FETCH (UID 10 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:30:25 -0400" RFC822.SIZE 298 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:30:25 -0400" "C" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {47} -References: - -) -* 11 FETCH (UID 11 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:31:25 -0400" RFC822.SIZE 282 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:31:25 -0400" "D" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {31} -References: - -) -* 12 FETCH (UID 12 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:32:25 -0400" RFC822.SIZE 330 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:32:25 -0400" "E" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {79} -References: - -) -* 13 FETCH (UID 13 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:33:25 -0400" RFC822.SIZE 282 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:33:25 -0400" "F" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {31} -References: - -) -* 14 FETCH (UID 14 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:34:25 -0400" RFC822.SIZE 253 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:34:25 -0400" "G" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {2} - -) -A00000039 OK Fetch completed (0.019 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/fetch12.txt b/UnitTests/Net/Imap/Resources/dovecot/fetch12.txt deleted file mode 100644 index ca7020cdc2..0000000000 --- a/UnitTests/Net/Imap/Resources/dovecot/fetch12.txt +++ /dev/null @@ -1,53 +0,0 @@ -* 1 FETCH (UID 1 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:28:25 -0400" RFC822.SIZE 253 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:28:25 -0400" "A" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {2} - -) -* 2 FETCH (UID 2 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:29:25 -0400" RFC822.SIZE 282 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:29:25 -0400" "B" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {31} -References: - -) -* 3 FETCH (UID 3 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:30:25 -0400" RFC822.SIZE 298 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:30:25 -0400" "C" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {47} -References: - -) -* 4 FETCH (UID 4 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:31:25 -0400" RFC822.SIZE 282 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:31:25 -0400" "D" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {31} -References: - -) -* 5 FETCH (UID 5 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:32:25 -0400" RFC822.SIZE 330 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:32:25 -0400" "E" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {79} -References: - -) -* 6 FETCH (UID 6 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:33:25 -0400" RFC822.SIZE 282 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:33:25 -0400" "F" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {31} -References: - -) -* 7 FETCH (UID 7 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:34:25 -0400" RFC822.SIZE 253 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:34:25 -0400" "G" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {2} - -) -* 8 FETCH (UID 8 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:28:25 -0400" RFC822.SIZE 253 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:28:25 -0400" "A" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {2} - -) -* 9 FETCH (UID 9 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:29:25 -0400" RFC822.SIZE 282 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:29:25 -0400" "B" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {31} -References: - -) -* 10 FETCH (UID 10 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:30:25 -0400" RFC822.SIZE 298 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:30:25 -0400" "C" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {47} -References: - -) -* 11 FETCH (UID 11 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:31:25 -0400" RFC822.SIZE 282 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:31:25 -0400" "D" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {31} -References: - -) -* 12 FETCH (UID 12 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:32:25 -0400" RFC822.SIZE 330 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:32:25 -0400" "E" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {79} -References: - -) -* 13 FETCH (UID 13 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:33:25 -0400" RFC822.SIZE 282 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:33:25 -0400" "F" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {31} -References: - -) -* 14 FETCH (UID 14 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:34:25 -0400" RFC822.SIZE 253 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:34:25 -0400" "G" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {2} - -) -A00000040 OK Fetch completed (0.019 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/fetch2.txt b/UnitTests/Net/Imap/Resources/dovecot/fetch2.txt index f7a2799589..6457aa0489 100644 --- a/UnitTests/Net/Imap/Resources/dovecot/fetch2.txt +++ b/UnitTests/Net/Imap/Resources/dovecot/fetch2.txt @@ -6,4 +6,4 @@ * 5 FETCH (UID 5 FLAGS (\Seen \Draft) MODSEQ (3)) * 6 FETCH (UID 6 FLAGS (\Seen \Draft) MODSEQ (3)) * 7 FETCH (UID 7 FLAGS (\Seen \Draft) MODSEQ (3)) -A00000022 OK Fetch completed (0.001 + 0.000 secs). +A######## OK Fetch completed (0.001 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/fetch3.txt b/UnitTests/Net/Imap/Resources/dovecot/fetch3.txt index ee424579dd..9a28a0b047 100644 --- a/UnitTests/Net/Imap/Resources/dovecot/fetch3.txt +++ b/UnitTests/Net/Imap/Resources/dovecot/fetch3.txt @@ -50,4 +50,4 @@ References: * 14 FETCH (UID 14 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:34:25 -0400" RFC822.SIZE 253 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:34:25 -0400" "G" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {2} ) -A00000031 OK Fetch completed (0.019 + 0.000 secs). +A######## OK Fetch completed (0.019 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/fetch4.txt b/UnitTests/Net/Imap/Resources/dovecot/fetch4.txt index eb857d545f..fb96114ae3 100644 --- a/UnitTests/Net/Imap/Resources/dovecot/fetch4.txt +++ b/UnitTests/Net/Imap/Resources/dovecot/fetch4.txt @@ -1,53 +1,53 @@ -* 1 FETCH (UID 1 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:28:25 -0400" RFC822.SIZE 253 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:28:25 -0400" "A" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {2} +* 1 FETCH (UID 1 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:28:25 -0400" RFC822.SIZE 253 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:28:25 -0400" "A" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {2} ) -* 2 FETCH (UID 2 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:29:25 -0400" RFC822.SIZE 282 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:29:25 -0400" "B" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {31} +* 2 FETCH (UID 2 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:29:25 -0400" RFC822.SIZE 282 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:29:25 -0400" "B" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE (("TEXT" "PLAIN" ("CHARSET" "US-ASCII") NIL NIL "7BIT" 1152 23)("TEXT" "PLAIN" ("CHARSET" "US-ASCII" "NAME" "cc.diff") "<960723163407.20117h@cac.washington.edu>" "Compiler diff" "BASE64" 4554 73) "MIXED") BODY[HEADER.FIELDS (REFERENCES)] {31} References: ) -* 3 FETCH (UID 3 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:30:25 -0400" RFC822.SIZE 298 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:30:25 -0400" "C" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {47} +* 3 FETCH (UID 3 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:30:25 -0400" RFC822.SIZE 298 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:30:25 -0400" "C" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {47} References: ) -* 4 FETCH (UID 4 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:31:25 -0400" RFC822.SIZE 282 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:31:25 -0400" "D" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {31} +* 4 FETCH (UID 4 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:31:25 -0400" RFC822.SIZE 282 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:31:25 -0400" "D" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {31} References: ) -* 5 FETCH (UID 5 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:32:25 -0400" RFC822.SIZE 330 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:32:25 -0400" "E" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {79} +* 5 FETCH (UID 5 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:32:25 -0400" RFC822.SIZE 330 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:32:25 -0400" "E" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {79} References: ) -* 6 FETCH (UID 6 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:33:25 -0400" RFC822.SIZE 282 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:33:25 -0400" "F" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {31} +* 6 FETCH (UID 6 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:33:25 -0400" RFC822.SIZE 282 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:33:25 -0400" "F" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {31} References: ) -* 7 FETCH (UID 7 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:34:25 -0400" RFC822.SIZE 253 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:34:25 -0400" "G" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {2} +* 7 FETCH (UID 7 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:34:25 -0400" RFC822.SIZE 253 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:34:25 -0400" "G" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {2} ) -* 8 FETCH (UID 8 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:28:25 -0400" RFC822.SIZE 253 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:28:25 -0400" "A" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {2} +* 8 FETCH (UID 8 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:28:25 -0400" RFC822.SIZE 253 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:28:25 -0400" "A" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {2} ) -* 9 FETCH (UID 9 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:29:25 -0400" RFC822.SIZE 282 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:29:25 -0400" "B" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {31} +* 9 FETCH (UID 9 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:29:25 -0400" RFC822.SIZE 282 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:29:25 -0400" "B" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {31} References: ) -* 10 FETCH (UID 10 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:30:25 -0400" RFC822.SIZE 298 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:30:25 -0400" "C" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {47} +* 10 FETCH (UID 10 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:30:25 -0400" RFC822.SIZE 298 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:30:25 -0400" "C" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {47} References: ) -* 11 FETCH (UID 11 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:31:25 -0400" RFC822.SIZE 282 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:31:25 -0400" "D" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {31} +* 11 FETCH (UID 11 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:31:25 -0400" RFC822.SIZE 282 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:31:25 -0400" "D" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {31} References: ) -* 12 FETCH (UID 12 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:32:25 -0400" RFC822.SIZE 330 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:32:25 -0400" "E" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {79} +* 12 FETCH (UID 12 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:32:25 -0400" RFC822.SIZE 330 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:32:25 -0400" "E" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {79} References: ) -* 13 FETCH (UID 13 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:33:25 -0400" RFC822.SIZE 282 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:33:25 -0400" "F" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {31} +* 13 FETCH (UID 13 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:33:25 -0400" RFC822.SIZE 282 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:33:25 -0400" "F" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {31} References: ) -* 14 FETCH (UID 14 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:34:25 -0400" RFC822.SIZE 253 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:34:25 -0400" "G" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {2} +* 14 FETCH (UID 14 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:34:25 -0400" RFC822.SIZE 253 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:34:25 -0400" "G" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {2} ) -A00000032 OK Fetch completed (0.019 + 0.000 secs). +A######## OK Fetch completed (0.019 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/fetch5.txt b/UnitTests/Net/Imap/Resources/dovecot/fetch5.txt deleted file mode 100644 index 8a6fa63ede..0000000000 --- a/UnitTests/Net/Imap/Resources/dovecot/fetch5.txt +++ /dev/null @@ -1,53 +0,0 @@ -* 1 FETCH (UID 1 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:28:25 -0400" RFC822.SIZE 253 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:28:25 -0400" "A" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {2} - -) -* 2 FETCH (UID 2 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:29:25 -0400" RFC822.SIZE 282 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:29:25 -0400" "B" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {31} -References: - -) -* 3 FETCH (UID 3 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:30:25 -0400" RFC822.SIZE 298 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:30:25 -0400" "C" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {47} -References: - -) -* 4 FETCH (UID 4 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:31:25 -0400" RFC822.SIZE 282 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:31:25 -0400" "D" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {31} -References: - -) -* 5 FETCH (UID 5 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:32:25 -0400" RFC822.SIZE 330 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:32:25 -0400" "E" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {79} -References: - -) -* 6 FETCH (UID 6 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:33:25 -0400" RFC822.SIZE 282 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:33:25 -0400" "F" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {31} -References: - -) -* 7 FETCH (UID 7 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:34:25 -0400" RFC822.SIZE 253 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:34:25 -0400" "G" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {2} - -) -* 8 FETCH (UID 8 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:28:25 -0400" RFC822.SIZE 253 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:28:25 -0400" "A" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {2} - -) -* 9 FETCH (UID 9 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:29:25 -0400" RFC822.SIZE 282 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:29:25 -0400" "B" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {31} -References: - -) -* 10 FETCH (UID 10 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:30:25 -0400" RFC822.SIZE 298 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:30:25 -0400" "C" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {47} -References: - -) -* 11 FETCH (UID 11 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:31:25 -0400" RFC822.SIZE 282 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:31:25 -0400" "D" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {31} -References: - -) -* 12 FETCH (UID 12 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:32:25 -0400" RFC822.SIZE 330 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:32:25 -0400" "E" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {79} -References: - -) -* 13 FETCH (UID 13 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:33:25 -0400" RFC822.SIZE 282 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:33:25 -0400" "F" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {31} -References: - -) -* 14 FETCH (UID 14 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:34:25 -0400" RFC822.SIZE 253 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:34:25 -0400" "G" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {2} - -) -A00000033 OK Fetch completed (0.019 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/fetch6.txt b/UnitTests/Net/Imap/Resources/dovecot/fetch6.txt deleted file mode 100644 index fc739902e4..0000000000 --- a/UnitTests/Net/Imap/Resources/dovecot/fetch6.txt +++ /dev/null @@ -1,53 +0,0 @@ -* 1 FETCH (UID 1 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:28:25 -0400" RFC822.SIZE 253 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:28:25 -0400" "A" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {2} - -) -* 2 FETCH (UID 2 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:29:25 -0400" RFC822.SIZE 282 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:29:25 -0400" "B" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {31} -References: - -) -* 3 FETCH (UID 3 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:30:25 -0400" RFC822.SIZE 298 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:30:25 -0400" "C" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {47} -References: - -) -* 4 FETCH (UID 4 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:31:25 -0400" RFC822.SIZE 282 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:31:25 -0400" "D" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {31} -References: - -) -* 5 FETCH (UID 5 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:32:25 -0400" RFC822.SIZE 330 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:32:25 -0400" "E" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {79} -References: - -) -* 6 FETCH (UID 6 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:33:25 -0400" RFC822.SIZE 282 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:33:25 -0400" "F" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {31} -References: - -) -* 7 FETCH (UID 7 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:34:25 -0400" RFC822.SIZE 253 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:34:25 -0400" "G" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {2} - -) -* 8 FETCH (UID 8 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:28:25 -0400" RFC822.SIZE 253 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:28:25 -0400" "A" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {2} - -) -* 9 FETCH (UID 9 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:29:25 -0400" RFC822.SIZE 282 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:29:25 -0400" "B" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {31} -References: - -) -* 10 FETCH (UID 10 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:30:25 -0400" RFC822.SIZE 298 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:30:25 -0400" "C" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {47} -References: - -) -* 11 FETCH (UID 11 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:31:25 -0400" RFC822.SIZE 282 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:31:25 -0400" "D" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {31} -References: - -) -* 12 FETCH (UID 12 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:32:25 -0400" RFC822.SIZE 330 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:32:25 -0400" "E" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {79} -References: - -) -* 13 FETCH (UID 13 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:33:25 -0400" RFC822.SIZE 282 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:33:25 -0400" "F" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {31} -References: - -) -* 14 FETCH (UID 14 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:34:25 -0400" RFC822.SIZE 253 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:34:25 -0400" "G" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {2} - -) -A00000034 OK Fetch completed (0.019 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/fetch7.txt b/UnitTests/Net/Imap/Resources/dovecot/fetch7.txt deleted file mode 100644 index 5db0542dcb..0000000000 --- a/UnitTests/Net/Imap/Resources/dovecot/fetch7.txt +++ /dev/null @@ -1,53 +0,0 @@ -* 1 FETCH (UID 1 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:28:25 -0400" RFC822.SIZE 253 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:28:25 -0400" "A" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {2} - -) -* 2 FETCH (UID 2 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:29:25 -0400" RFC822.SIZE 282 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:29:25 -0400" "B" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {31} -References: - -) -* 3 FETCH (UID 3 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:30:25 -0400" RFC822.SIZE 298 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:30:25 -0400" "C" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {47} -References: - -) -* 4 FETCH (UID 4 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:31:25 -0400" RFC822.SIZE 282 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:31:25 -0400" "D" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {31} -References: - -) -* 5 FETCH (UID 5 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:32:25 -0400" RFC822.SIZE 330 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:32:25 -0400" "E" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {79} -References: - -) -* 6 FETCH (UID 6 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:33:25 -0400" RFC822.SIZE 282 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:33:25 -0400" "F" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {31} -References: - -) -* 7 FETCH (UID 7 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:34:25 -0400" RFC822.SIZE 253 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:34:25 -0400" "G" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {2} - -) -* 8 FETCH (UID 8 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:28:25 -0400" RFC822.SIZE 253 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:28:25 -0400" "A" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {2} - -) -* 9 FETCH (UID 9 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:29:25 -0400" RFC822.SIZE 282 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:29:25 -0400" "B" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {31} -References: - -) -* 10 FETCH (UID 10 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:30:25 -0400" RFC822.SIZE 298 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:30:25 -0400" "C" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {47} -References: - -) -* 11 FETCH (UID 11 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:31:25 -0400" RFC822.SIZE 282 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:31:25 -0400" "D" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {31} -References: - -) -* 12 FETCH (UID 12 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:32:25 -0400" RFC822.SIZE 330 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:32:25 -0400" "E" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {79} -References: - -) -* 13 FETCH (UID 13 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:33:25 -0400" RFC822.SIZE 282 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:33:25 -0400" "F" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {31} -References: - -) -* 14 FETCH (UID 14 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:34:25 -0400" RFC822.SIZE 253 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:34:25 -0400" "G" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES)] {2} - -) -A00000035 OK Fetch completed (0.019 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/fetch8.txt b/UnitTests/Net/Imap/Resources/dovecot/fetch8.txt deleted file mode 100644 index 275dc5653e..0000000000 --- a/UnitTests/Net/Imap/Resources/dovecot/fetch8.txt +++ /dev/null @@ -1,53 +0,0 @@ -* 1 FETCH (UID 1 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:28:25 -0400" RFC822.SIZE 253 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:28:25 -0400" "A" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {2} - -) -* 2 FETCH (UID 2 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:29:25 -0400" RFC822.SIZE 282 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:29:25 -0400" "B" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {31} -References: - -) -* 3 FETCH (UID 3 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:30:25 -0400" RFC822.SIZE 298 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:30:25 -0400" "C" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {47} -References: - -) -* 4 FETCH (UID 4 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:31:25 -0400" RFC822.SIZE 282 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:31:25 -0400" "D" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {31} -References: - -) -* 5 FETCH (UID 5 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:32:25 -0400" RFC822.SIZE 330 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:32:25 -0400" "E" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {79} -References: - -) -* 6 FETCH (UID 6 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:33:25 -0400" RFC822.SIZE 282 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:33:25 -0400" "F" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {31} -References: - -) -* 7 FETCH (UID 7 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:34:25 -0400" RFC822.SIZE 253 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:34:25 -0400" "G" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {2} - -) -* 8 FETCH (UID 8 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:28:25 -0400" RFC822.SIZE 253 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:28:25 -0400" "A" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {2} - -) -* 9 FETCH (UID 9 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:29:25 -0400" RFC822.SIZE 282 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:29:25 -0400" "B" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {31} -References: - -) -* 10 FETCH (UID 10 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:30:25 -0400" RFC822.SIZE 298 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:30:25 -0400" "C" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {47} -References: - -) -* 11 FETCH (UID 11 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:31:25 -0400" RFC822.SIZE 282 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:31:25 -0400" "D" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {31} -References: - -) -* 12 FETCH (UID 12 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:32:25 -0400" RFC822.SIZE 330 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:32:25 -0400" "E" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {79} -References: - -) -* 13 FETCH (UID 13 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:33:25 -0400" RFC822.SIZE 282 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:33:25 -0400" "F" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {31} -References: - -) -* 14 FETCH (UID 14 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:34:25 -0400" RFC822.SIZE 253 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:34:25 -0400" "G" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {2} - -) -A00000036 OK Fetch completed (0.019 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/fetch9.txt b/UnitTests/Net/Imap/Resources/dovecot/fetch9.txt deleted file mode 100644 index 8ea6e4d977..0000000000 --- a/UnitTests/Net/Imap/Resources/dovecot/fetch9.txt +++ /dev/null @@ -1,53 +0,0 @@ -* 1 FETCH (UID 1 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:28:25 -0400" RFC822.SIZE 253 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:28:25 -0400" "A" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {2} - -) -* 2 FETCH (UID 2 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:29:25 -0400" RFC822.SIZE 282 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:29:25 -0400" "B" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {31} -References: - -) -* 3 FETCH (UID 3 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:30:25 -0400" RFC822.SIZE 298 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:30:25 -0400" "C" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {47} -References: - -) -* 4 FETCH (UID 4 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:31:25 -0400" RFC822.SIZE 282 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:31:25 -0400" "D" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {31} -References: - -) -* 5 FETCH (UID 5 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:32:25 -0400" RFC822.SIZE 330 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:32:25 -0400" "E" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {79} -References: - -) -* 6 FETCH (UID 6 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:33:25 -0400" RFC822.SIZE 282 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:33:25 -0400" "F" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {31} -References: - -) -* 7 FETCH (UID 7 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:34:25 -0400" RFC822.SIZE 253 MODSEQ (2) ENVELOPE ("Sun, 02 Oct 2016 17:34:25 -0400" "G" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {2} - -) -* 8 FETCH (UID 8 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:28:25 -0400" RFC822.SIZE 253 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:28:25 -0400" "A" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {2} - -) -* 9 FETCH (UID 9 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:29:25 -0400" RFC822.SIZE 282 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:29:25 -0400" "B" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {31} -References: - -) -* 10 FETCH (UID 10 FLAGS (\Answered \Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:30:25 -0400" RFC822.SIZE 298 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:30:25 -0400" "C" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {47} -References: - -) -* 11 FETCH (UID 11 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:31:25 -0400" RFC822.SIZE 282 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:31:25 -0400" "D" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {31} -References: - -) -* 12 FETCH (UID 12 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:32:25 -0400" RFC822.SIZE 330 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:32:25 -0400" "E" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {79} -References: - -) -* 13 FETCH (UID 13 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:33:25 -0400" RFC822.SIZE 282 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:33:25 -0400" "F" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {31} -References: - -) -* 14 FETCH (UID 14 FLAGS (\Seen \Draft \Recent) INTERNALDATE "02-Oct-2016 17:34:25 -0400" RFC822.SIZE 253 MODSEQ (3) ENVELOPE ("Sun, 02 Oct 2016 17:34:25 -0400" "G" (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) (("Unit Tests" NIL "unit-tests" "mimekit.net")) NIL NIL NIL "") BODYSTRUCTURE ("text" "plain" ("charset" "utf-8") NIL NIL "7bit" 27 1 NIL NIL NIL NIL) BODY[HEADER.FIELDS (REFERENCES X-MAILER)] {2} - -) -A00000037 OK Fetch completed (0.019 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/getbodypart.txt b/UnitTests/Net/Imap/Resources/dovecot/getbodypart.txt index ae73eb3964..2a98d73d2f 100644 --- a/UnitTests/Net/Imap/Resources/dovecot/getbodypart.txt +++ b/UnitTests/Net/Imap/Resources/dovecot/getbodypart.txt @@ -1,4 +1,4 @@ -* 1 FETCH (UID 1 BODY[HEADER] {226} +* 1 FETCH (UID 1 BODY[] {253} From: Unit Tests Date: Sun, 02 Oct 2016 17:56:45 -0400 Subject: A @@ -7,7 +7,6 @@ To: Unit Tests MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 - BODY[TEXT] {27} This is the message body. ) -A00000041 OK Fetch completed (0.002 + 0.000 secs). +A######## OK Fetch completed (0.002 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/getbodypart1.txt b/UnitTests/Net/Imap/Resources/dovecot/getbodypart1.txt new file mode 100644 index 0000000000..e6abbc302c --- /dev/null +++ b/UnitTests/Net/Imap/Resources/dovecot/getbodypart1.txt @@ -0,0 +1,7 @@ +* 2 FETCH (UID 2 BODY[1.MIME] {43} +Content-Type: text/plain; charset=utf-8 + + BODY[1] {27} +This is the message body. +) +A######## OK Fetch completed (0.002 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/getbodypart2.txt b/UnitTests/Net/Imap/Resources/dovecot/getbodypart2.txt deleted file mode 100644 index 64a92997b0..0000000000 --- a/UnitTests/Net/Imap/Resources/dovecot/getbodypart2.txt +++ /dev/null @@ -1,13 +0,0 @@ -* 1 FETCH (UID 1 BODY[HEADER] {226} -From: Unit Tests -Date: Sun, 02 Oct 2016 17:56:45 -0400 -Subject: A -Message-Id: -To: Unit Tests -MIME-Version: 1.0 -Content-Type: text/plain; charset=utf-8 - - BODY[TEXT] {27} -This is the message body. -) -A00000042 OK Fetch completed (0.002 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/getbodypartheaders.txt b/UnitTests/Net/Imap/Resources/dovecot/getbodypartheaders.txt index 9af1f421ee..b3a2e397e5 100644 --- a/UnitTests/Net/Imap/Resources/dovecot/getbodypartheaders.txt +++ b/UnitTests/Net/Imap/Resources/dovecot/getbodypartheaders.txt @@ -1,11 +1,5 @@ -* 1 FETCH (UID 1 BODY[HEADER] {226} -From: Unit Tests -Date: Sun, 02 Oct 2016 17:56:45 -0400 -Subject: A -Message-Id: -To: Unit Tests -MIME-Version: 1.0 +* 2 FETCH (BODY[1.MIME] {43} Content-Type: text/plain; charset=utf-8 - ) -A00000045 OK Fetch completed (0.002 + 0.000 secs). + UID 2) +A######## OK Fetch completed (0.002 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/getbodypartheaders2.txt b/UnitTests/Net/Imap/Resources/dovecot/getbodypartheaders2.txt deleted file mode 100644 index cdd981f989..0000000000 --- a/UnitTests/Net/Imap/Resources/dovecot/getbodypartheaders2.txt +++ /dev/null @@ -1,11 +0,0 @@ -* 1 FETCH (UID 1 BODY[HEADER] {226} -From: Unit Tests -Date: Sun, 02 Oct 2016 17:56:45 -0400 -Subject: A -Message-Id: -To: Unit Tests -MIME-Version: 1.0 -Content-Type: text/plain; charset=utf-8 - - ) -A00000046 OK Fetch completed (0.002 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/getmessageheaders.txt b/UnitTests/Net/Imap/Resources/dovecot/getmessageheaders.txt index cccb0014fb..ea9aec5210 100644 --- a/UnitTests/Net/Imap/Resources/dovecot/getmessageheaders.txt +++ b/UnitTests/Net/Imap/Resources/dovecot/getmessageheaders.txt @@ -1,4 +1,4 @@ -* 1 FETCH (UID 1 BODY[HEADER] {226} +* 1 FETCH (BODY[HEADER] {226} From: Unit Tests Date: Sun, 02 Oct 2016 17:56:45 -0400 Subject: A @@ -7,5 +7,5 @@ To: Unit Tests MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 - ) -A00000043 OK Fetch completed (0.002 + 0.000 secs). + UID 1) +A######## OK Fetch completed (0.002 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/getmessageheaders2.txt b/UnitTests/Net/Imap/Resources/dovecot/getmessageheaders2.txt deleted file mode 100644 index 63294a7beb..0000000000 --- a/UnitTests/Net/Imap/Resources/dovecot/getmessageheaders2.txt +++ /dev/null @@ -1,11 +0,0 @@ -* 1 FETCH (UID 1 BODY[HEADER] {226} -From: Unit Tests -Date: Sun, 02 Oct 2016 17:56:45 -0400 -Subject: A -Message-Id: -To: Unit Tests -MIME-Version: 1.0 -Content-Type: text/plain; charset=utf-8 - - ) -A00000044 OK Fetch completed (0.002 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/getstream-section.txt b/UnitTests/Net/Imap/Resources/dovecot/getstream-section.txt index 9c699f8b7d..49164865b9 100644 --- a/UnitTests/Net/Imap/Resources/dovecot/getstream-section.txt +++ b/UnitTests/Net/Imap/Resources/dovecot/getstream-section.txt @@ -3,4 +3,4 @@ MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 ) -A00000051 OK Fetch completed (0.002 + 0.000 secs). +A######## OK Fetch completed (0.002 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/getstream-section2.txt b/UnitTests/Net/Imap/Resources/dovecot/getstream-section2.txt index 6645f32e90..49164865b9 100644 --- a/UnitTests/Net/Imap/Resources/dovecot/getstream-section2.txt +++ b/UnitTests/Net/Imap/Resources/dovecot/getstream-section2.txt @@ -3,4 +3,4 @@ MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 ) -A00000052 OK Fetch completed (0.002 + 0.000 secs). +A######## OK Fetch completed (0.002 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/getstream.txt b/UnitTests/Net/Imap/Resources/dovecot/getstream.txt index ceea5be610..a4428faa1b 100644 --- a/UnitTests/Net/Imap/Resources/dovecot/getstream.txt +++ b/UnitTests/Net/Imap/Resources/dovecot/getstream.txt @@ -2,4 +2,4 @@ nit Tests MIME-Version: 1.0 Content-T) -A00000047 OK Fetch completed (0.002 + 0.000 secs). +A######## OK Fetch completed (0.002 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/getstream2.txt b/UnitTests/Net/Imap/Resources/dovecot/getstream2.txt index df4ebb969b..0b6a398339 100644 --- a/UnitTests/Net/Imap/Resources/dovecot/getstream2.txt +++ b/UnitTests/Net/Imap/Resources/dovecot/getstream2.txt @@ -1,5 +1,5 @@ -* 1 FETCH (UID 1 BODY[]<128> {64} +* 1 FETCH (BODY[]<128> {64} nit Tests MIME-Version: 1.0 -Content-T) -A00000048 OK Fetch completed (0.002 + 0.000 secs). +Content-T UID 1) +A######## OK Fetch completed (0.002 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/getstream3.txt b/UnitTests/Net/Imap/Resources/dovecot/getstream3.txt deleted file mode 100644 index a708659778..0000000000 --- a/UnitTests/Net/Imap/Resources/dovecot/getstream3.txt +++ /dev/null @@ -1,5 +0,0 @@ -* 1 FETCH (UID 1 BODY[]<128> {64} -nit Tests -MIME-Version: 1.0 -Content-T) -A00000049 OK Fetch completed (0.002 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/getstream4.txt b/UnitTests/Net/Imap/Resources/dovecot/getstream4.txt deleted file mode 100644 index 0192f35ef0..0000000000 --- a/UnitTests/Net/Imap/Resources/dovecot/getstream4.txt +++ /dev/null @@ -1,5 +0,0 @@ -* 1 FETCH (UID 1 BODY[]<128> {64} -nit Tests -MIME-Version: 1.0 -Content-T) -A00000050 OK Fetch completed (0.002 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/getstreams1.txt b/UnitTests/Net/Imap/Resources/dovecot/getstreams1.txt new file mode 100644 index 0000000000..61ac3c9aed --- /dev/null +++ b/UnitTests/Net/Imap/Resources/dovecot/getstreams1.txt @@ -0,0 +1,7 @@ +* 1 FETCH (UID 1 BODY[] {68} +This is some dummy text just to make sure this is working correctly.) +* 2 FETCH (UID 2 BODY[] {68} +This is some dummy text just to make sure this is working correctly.) +* 3 FETCH (UID 3 BODY[] {68} +This is some dummy text just to make sure this is working correctly.) +A######## OK Fetch completed (0.002 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/getstreams2.txt b/UnitTests/Net/Imap/Resources/dovecot/getstreams2.txt new file mode 100644 index 0000000000..9349d39d5e --- /dev/null +++ b/UnitTests/Net/Imap/Resources/dovecot/getstreams2.txt @@ -0,0 +1,7 @@ +* 1 FETCH (BODY[] {68} +This is some dummy text just to make sure this is working correctly. UID 1) +* 2 FETCH (BODY[] {68} +This is some dummy text just to make sure this is working correctly. UID 2) +* 3 FETCH (BODY[] {68} +This is some dummy text just to make sure this is working correctly. UID 3) +A######## OK Fetch completed (0.002 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/greeting-preauth.txt b/UnitTests/Net/Imap/Resources/dovecot/greeting-preauth.txt new file mode 100644 index 0000000000..06e9567bc8 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/dovecot/greeting-preauth.txt @@ -0,0 +1 @@ +* PREAUTH [CAPABILITY IMAP4rev1 LITERAL+ SASL-IR LOGIN-REFERRALS ID ENABLE IDLE SORT SORT=DISPLAY THREAD=REFERENCES THREAD=REFS THREAD=ORDEREDSUBJECT MULTIAPPEND URL-PARTIAL CATENATE UNSELECT CHILDREN NAMESPACE UIDPLUS LIST-EXTENDED I18NLEVEL=1 CONDSTORE QRESYNC ESEARCH ESORT SEARCHRES WITHIN CONTEXT=SEARCH LIST-STATUS BINARY MOVE SPECIAL-USE NOTIFY] Logged in as user diff --git a/UnitTests/Net/Imap/Resources/dovecot/list-folder.txt b/UnitTests/Net/Imap/Resources/dovecot/list-folder.txt new file mode 100644 index 0000000000..0de3c8d3d8 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/dovecot/list-folder.txt @@ -0,0 +1,2 @@ +* LIST (\HasNoChildren) "." Folder +A######## OK List completed (0.000 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/list-inbox.txt b/UnitTests/Net/Imap/Resources/dovecot/list-inbox.txt index 07b6d1f396..c51262e7ed 100644 --- a/UnitTests/Net/Imap/Resources/dovecot/list-inbox.txt +++ b/UnitTests/Net/Imap/Resources/dovecot/list-inbox.txt @@ -1,2 +1,2 @@ -* LIST (\HasNoChildren) "." INBOX -A00000002 OK List completed (0.000 + 0.000 secs). +* LIST (\HasNoChildren \Subscribed) "." INBOX +A######## OK List completed (0.000 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/list-personal.txt b/UnitTests/Net/Imap/Resources/dovecot/list-personal.txt index 3264eaef36..0e885d3eb4 100644 --- a/UnitTests/Net/Imap/Resources/dovecot/list-personal.txt +++ b/UnitTests/Net/Imap/Resources/dovecot/list-personal.txt @@ -12,4 +12,4 @@ * STATUS INBOX (MESSAGES 4 RECENT 0 UIDNEXT 5 UIDVALIDITY 1436832057 UNSEEN 0 HIGHESTMODSEQ 15) * LIST (\HasNoChildren) "." NIL * STATUS NIL (MESSAGES 0 RECENT 0 UIDNEXT 1 UIDVALIDITY 1436832057 UNSEEN 0 HIGHESTMODSEQ 1) -A00000005 OK List completed (0.000 + 0.000 secs). +A######## OK List completed (0.000 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/list-special-use.txt b/UnitTests/Net/Imap/Resources/dovecot/list-special-use.txt index 2b63e11456..4acf900200 100644 --- a/UnitTests/Net/Imap/Resources/dovecot/list-special-use.txt +++ b/UnitTests/Net/Imap/Resources/dovecot/list-special-use.txt @@ -1,5 +1,5 @@ -* LIST (\Drafts) "." Drafts -* LIST (\Junk) "." Junk -* LIST (\Sent) "." "Sent Messages" -* LIST (\Trash) "." Trash -A00000003 OK List completed (0.000 + 0.000 secs). +* LIST (\Drafts \Subscribed) "." Drafts +* LIST (\Junk \Subscribed) "." Junk +* LIST (\Sent \Subscribed) "." "Sent Messages" +* LIST (\Trash \Subscribed) "." Trash +A######## OK List completed (0.000 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/list-unittests-destination.txt b/UnitTests/Net/Imap/Resources/dovecot/list-unittests-destination.txt index 749704efce..846680baa8 100644 --- a/UnitTests/Net/Imap/Resources/dovecot/list-unittests-destination.txt +++ b/UnitTests/Net/Imap/Resources/dovecot/list-unittests-destination.txt @@ -1,2 +1,2 @@ * LIST (\HasNoChildren) "." UnitTests.Destination -A00000026 OK List completed (0.000 + 0.000 secs). +A######## OK List completed (0.000 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/list-unittests-messages.txt b/UnitTests/Net/Imap/Resources/dovecot/list-unittests-messages.txt index af174a189f..87c79ac374 100644 --- a/UnitTests/Net/Imap/Resources/dovecot/list-unittests-messages.txt +++ b/UnitTests/Net/Imap/Resources/dovecot/list-unittests-messages.txt @@ -1,2 +1,2 @@ * LIST (\HasNoChildren) "." UnitTests.Messages -A00000009 OK List completed (0.000 + 0.000 secs). +A######## OK List completed (0.000 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/list-unittests.txt b/UnitTests/Net/Imap/Resources/dovecot/list-unittests.txt index 910186f44e..c604a1fb26 100644 --- a/UnitTests/Net/Imap/Resources/dovecot/list-unittests.txt +++ b/UnitTests/Net/Imap/Resources/dovecot/list-unittests.txt @@ -1,2 +1,2 @@ * LIST (\HasNoChildren) "." UnitTests -A00000007 OK List completed (0.000 + 0.000 secs). +A######## OK List completed (0.000 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/move.txt b/UnitTests/Net/Imap/Resources/dovecot/move.txt index 147e6ad050..229206505c 100644 --- a/UnitTests/Net/Imap/Resources/dovecot/move.txt +++ b/UnitTests/Net/Imap/Resources/dovecot/move.txt @@ -1,3 +1,3 @@ * OK [COPYUID 1436832101 1:7 8:14] Moved UIDs. * VANISHED 1:7 -A00000028 OK [HIGHESTMODSEQ 7] Move completed (0.013 + 0.000 secs). +A######## OK [HIGHESTMODSEQ 7] Move completed (0.013 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/multiappend.txt b/UnitTests/Net/Imap/Resources/dovecot/multiappend.txt index 3b51234ae8..bc273f47ea 100644 --- a/UnitTests/Net/Imap/Resources/dovecot/multiappend.txt +++ b/UnitTests/Net/Imap/Resources/dovecot/multiappend.txt @@ -1 +1 @@ -A00000010 OK [APPENDUID 1436832084 1:8] Append completed (0.000 + 0.000 secs). +A######## OK [APPENDUID 1436832084 1:8] Append completed (0.000 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/namespace.txt b/UnitTests/Net/Imap/Resources/dovecot/namespace.txt index c9520127dd..2a6bc87507 100644 --- a/UnitTests/Net/Imap/Resources/dovecot/namespace.txt +++ b/UnitTests/Net/Imap/Resources/dovecot/namespace.txt @@ -1,2 +1,2 @@ * NAMESPACE (("" ".")) NIL NIL -A00000001 OK Namespace completed. +A######## OK Namespace completed. diff --git a/UnitTests/Net/Imap/Resources/dovecot/noop+alert.txt b/UnitTests/Net/Imap/Resources/dovecot/noop+alert.txt new file mode 100644 index 0000000000..459715675f --- /dev/null +++ b/UnitTests/Net/Imap/Resources/dovecot/noop+alert.txt @@ -0,0 +1,2 @@ +* OK [ALERT] System shutdown in 10 minutes +A######## OK NOOP complete. diff --git a/UnitTests/Net/Imap/Resources/dovecot/notify-idle-done.txt b/UnitTests/Net/Imap/Resources/dovecot/notify-idle-done.txt new file mode 100644 index 0000000000..71f28f33b1 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/dovecot/notify-idle-done.txt @@ -0,0 +1 @@ +A######## OK Idle completed (0.001 + 5.433 + 5.433 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/notify-idle.txt b/UnitTests/Net/Imap/Resources/dovecot/notify-idle.txt new file mode 100644 index 0000000000..e08df4c3f2 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/dovecot/notify-idle.txt @@ -0,0 +1,12 @@ ++ idling +* STATUS INBOX (MESSAGES 3 UIDNEXT 4 UNSEEN 3 HIGHESTMODSEQ 3) +* LIST (\HasNoChildren) "." NewFolder +* LIST (\NonExistent) "." DeleteMe +* LIST (\HasNoChildren) "." RenamedFolder ("OLDNAME" ("RenameMe")) +* LIST (\HasNoChildren) "." UnsubscribeMe +* LIST (\HasNoChildren \Subscribed) "." SubscribeMe +* METADATA "" (/private/comment "this is a comment") +* METADATA INBOX (/private/comment "this is a comment") +* 1 EXISTS +* 1 RECENT +* 1 FETCH (UID 1 FLAGS (\Recent) ENVELOPE ("Wed, 17 Jul 1996 02:23:25 -0700 (PDT)" "IMAP4rev1 WG mtg summary and minutes" (("Terry Gray" NIL "gray" "cac.washington.edu")) (("Terry Gray" NIL "gray" "cac.washington.edu")) (("Terry Gray" NIL "gray" "cac.washington.edu")) ((NIL NIL "imap" "cac.washington.edu")) ((NIL NIL "minutes" "CNRI.Reston.VA.US") ("John Klensin" NIL "KLENSIN" "MIT.EDU")) NIL NIL "") BODYSTRUCTURE ("TEXT" "PLAIN" ("CHARSET" "US-ASCII") NIL NIL "7BIT" 3028 92) MODSEQ (1)) diff --git a/UnitTests/Net/Imap/Resources/dovecot/notify-list-personal.txt b/UnitTests/Net/Imap/Resources/dovecot/notify-list-personal.txt new file mode 100644 index 0000000000..8857182b06 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/dovecot/notify-list-personal.txt @@ -0,0 +1,12 @@ +* LIST (\HasNoChildren) "." Archives +* LIST (\HasNoChildren) "." Drafts +* LIST (\HasNoChildren) "." Junk +* LIST (\HasNoChildren) "." "Sent Messages" +* LIST (\HasNoChildren) "." Trash +* LIST (\HasNoChildren) "." INBOX +* LIST (\HasChildren) "." Folder +* LIST (\HasNoChildren) "." DeleteMe +* LIST (\HasNoChildren) "." RenameMe +* LIST (\HasNoChildren) "." SubscribeMe +* LIST (\HasNoChildren \Subscribed) "." UnsubscribeMe +A######## OK List completed (0.000 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/notify.txt b/UnitTests/Net/Imap/Resources/dovecot/notify.txt new file mode 100644 index 0000000000..a0cac6bc34 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/dovecot/notify.txt @@ -0,0 +1,2 @@ +* STATUS INBOX (MESSAGES 1 UIDNEXT 2 UIDVALIDITY 1543354379 UNSEEN 1) +A######## OK NOTIFY completed (0.001 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/optimized-search.txt b/UnitTests/Net/Imap/Resources/dovecot/optimized-search.txt index 641df84e49..422098bb26 100644 --- a/UnitTests/Net/Imap/Resources/dovecot/optimized-search.txt +++ b/UnitTests/Net/Imap/Resources/dovecot/optimized-search.txt @@ -1,2 +1,2 @@ -* ESEARCH (TAG "A00000024") UID ALL 1:7 -A00000024 OK Search completed (0.001 + 0.000 secs). +* ESEARCH (TAG "A########") UID ALL 1:7 +A######## OK Search completed (0.001 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/search-all.txt b/UnitTests/Net/Imap/Resources/dovecot/search-all.txt index 807a8dd49f..384527911b 100644 --- a/UnitTests/Net/Imap/Resources/dovecot/search-all.txt +++ b/UnitTests/Net/Imap/Resources/dovecot/search-all.txt @@ -1,2 +1,2 @@ -* ESEARCH (TAG "A00000030") UID ALL 1:14 -A00000030 OK Search completed (0.001 + 0.000 secs). +* ESEARCH (TAG "A########") UID ALL 1:14 +A######## OK Search completed (0.001 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/search-changed-since.txt b/UnitTests/Net/Imap/Resources/dovecot/search-changed-since.txt index 38a8c821ff..d6933e5e3f 100644 --- a/UnitTests/Net/Imap/Resources/dovecot/search-changed-since.txt +++ b/UnitTests/Net/Imap/Resources/dovecot/search-changed-since.txt @@ -1,2 +1,2 @@ -* ESEARCH (TAG "A00000020") UID MIN 1 MAX 7 ALL 1:7 COUNT 7 MODSEQ 4 -A00000020 OK Search completed (0.001 + 0.000 secs). +* ESEARCH (TAG "A########") UID MIN 1 MAX 7 ALL 1:7 COUNT 7 MODSEQ 4 RELEVANCY (11 22 33 44 55 66 77) +A######## OK Search completed (0.001 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/search-raw.txt b/UnitTests/Net/Imap/Resources/dovecot/search-raw.txt index 96fa427827..4ad07d9038 100644 --- a/UnitTests/Net/Imap/Resources/dovecot/search-raw.txt +++ b/UnitTests/Net/Imap/Resources/dovecot/search-raw.txt @@ -1,2 +1,2 @@ -* SEARCH 1 2 3 4 5 6 7 8 9 10 11 12 13 14 -A00000060 OK Search completed (0.001 + 0.000 secs). +* SEARCH 1 2 3 4 5 6 7 8 9 10 11 12 13 14 (MODSEQ 12345678) +A######## OK Search completed (0.001 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/search-uids-options.txt b/UnitTests/Net/Imap/Resources/dovecot/search-uids-options.txt index 923feec28d..e17b709203 100644 --- a/UnitTests/Net/Imap/Resources/dovecot/search-uids-options.txt +++ b/UnitTests/Net/Imap/Resources/dovecot/search-uids-options.txt @@ -1,2 +1,2 @@ -* ESEARCH (TAG "A00000056") UID MIN 2 MAX 13 ALL 2:6,9:13 COUNT 10 -A00000056 OK Search completed (0.001 + 0.000 secs). +* ESEARCH (TAG "A########") UID MIN 2 MAX 13 ALL 2:6,9:13 COUNT 10 RELEVANCY (2 3 4 5 6 9 10 11 12 13) +A######## OK Search completed (0.001 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/search-uids.txt b/UnitTests/Net/Imap/Resources/dovecot/search-uids.txt index fadab349bd..384527911b 100644 --- a/UnitTests/Net/Imap/Resources/dovecot/search-uids.txt +++ b/UnitTests/Net/Imap/Resources/dovecot/search-uids.txt @@ -1,2 +1,2 @@ -* ESEARCH (TAG "A00000055") UID ALL 1:14 -A00000055 OK Search completed (0.001 + 0.000 secs). +* ESEARCH (TAG "A########") UID ALL 1:14 +A######## OK Search completed (0.001 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/select-unittests-destination.txt b/UnitTests/Net/Imap/Resources/dovecot/select-unittests-destination.txt index e16c2131fb..b2677a97e7 100644 --- a/UnitTests/Net/Imap/Resources/dovecot/select-unittests-destination.txt +++ b/UnitTests/Net/Imap/Resources/dovecot/select-unittests-destination.txt @@ -6,4 +6,4 @@ * OK [UIDVALIDITY 1436832128] UIDs valid * OK [UIDNEXT 15] Predicted next UID * OK [HIGHESTMODSEQ 3] Highest -A00000030 OK [READ-WRITE] Select completed (0.000 + 0.000 secs). +A######## OK [READ-WRITE] Select completed (0.000 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/select-unittests-messages-qresync.txt b/UnitTests/Net/Imap/Resources/dovecot/select-unittests-messages-qresync.txt index 0debfaa015..42a295b77c 100644 --- a/UnitTests/Net/Imap/Resources/dovecot/select-unittests-messages-qresync.txt +++ b/UnitTests/Net/Imap/Resources/dovecot/select-unittests-messages-qresync.txt @@ -13,4 +13,4 @@ * 5 FETCH (UID 5 FLAGS (\Seen \Draft) MODSEQ (3)) * 6 FETCH (UID 6 FLAGS (\Seen \Draft) MODSEQ (3)) * 7 FETCH (UID 7 FLAGS (\Seen \Draft) MODSEQ (3)) -A00000019 OK [READ-WRITE] Select completed (0.000 + 0.000 secs). +A######## OK [READ-WRITE] Select completed (0.000 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/select-unittests-messages.txt b/UnitTests/Net/Imap/Resources/dovecot/select-unittests-messages.txt index 5e0bb6eb17..f5468593ba 100644 --- a/UnitTests/Net/Imap/Resources/dovecot/select-unittests-messages.txt +++ b/UnitTests/Net/Imap/Resources/dovecot/select-unittests-messages.txt @@ -6,4 +6,4 @@ * OK [UIDVALIDITY 1436832084] UIDs valid * OK [UIDNEXT 9] Predicted next UID * OK [HIGHESTMODSEQ 2] Highest -A00000011 OK [READ-WRITE] Select completed (0.000 + 0.000 secs). +A######## OK [READ-WRITE] Select completed (0.000 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/setflags-unchangedsince.txt b/UnitTests/Net/Imap/Resources/dovecot/setflags-unchangedsince.txt index 2286e86de8..d63c4e76d4 100644 --- a/UnitTests/Net/Imap/Resources/dovecot/setflags-unchangedsince.txt +++ b/UnitTests/Net/Imap/Resources/dovecot/setflags-unchangedsince.txt @@ -5,4 +5,4 @@ * 5 FETCH (UID 5 MODSEQ (6)) * 6 FETCH (UID 6 MODSEQ (6)) * 7 FETCH (UID 7 MODSEQ (6)) -A00000054 OK Store completed (0.001 + 0.000 secs). +A######## OK [MODIFIED 7,9] Store completed (0.001 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/sort-by-date.txt b/UnitTests/Net/Imap/Resources/dovecot/sort-by-date.txt index 8700efe8c8..36b2340b22 100644 --- a/UnitTests/Net/Imap/Resources/dovecot/sort-by-date.txt +++ b/UnitTests/Net/Imap/Resources/dovecot/sort-by-date.txt @@ -1,2 +1,2 @@ -* ESEARCH (TAG "A00000057") UID ALL 7,14,6,13,5,12,4,11,3,10,2,9,1,8 -A00000057 OK Sort completed (0.002 + 0.000 secs). +* ESEARCH (TAG "A########") UID ALL 7,14,6,13,5,12,4,11,3,10,2,9,1,8 +A######## OK Sort completed (0.002 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/sort-by-strings.txt b/UnitTests/Net/Imap/Resources/dovecot/sort-by-strings.txt index 54b064deb8..29995c4432 100644 --- a/UnitTests/Net/Imap/Resources/dovecot/sort-by-strings.txt +++ b/UnitTests/Net/Imap/Resources/dovecot/sort-by-strings.txt @@ -1,2 +1,2 @@ -* ESEARCH (TAG "A00000058") UID ALL 1:14 -A00000058 OK Sort completed (0.002 + 0.000 secs). +* ESEARCH (TAG "A########") UID ALL 1:14 +A######## OK Sort completed (0.002 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/sort-raw.txt b/UnitTests/Net/Imap/Resources/dovecot/sort-raw.txt index a32136b05a..f7143ffc88 100644 --- a/UnitTests/Net/Imap/Resources/dovecot/sort-raw.txt +++ b/UnitTests/Net/Imap/Resources/dovecot/sort-raw.txt @@ -1,2 +1,2 @@ * SORT 7 14 6 13 5 12 4 11 3 10 2 9 1 8 -A00000061 OK Sort completed (0.001 + 0.000 secs). +A######## OK Sort completed (0.001 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/sort-reverse-arrival.txt b/UnitTests/Net/Imap/Resources/dovecot/sort-reverse-arrival.txt index 2997072c96..30c2ed3980 100644 --- a/UnitTests/Net/Imap/Resources/dovecot/sort-reverse-arrival.txt +++ b/UnitTests/Net/Imap/Resources/dovecot/sort-reverse-arrival.txt @@ -1,2 +1,2 @@ -* ESEARCH (TAG "A00000023") UID MIN 7 MAX 1 ALL 7,6,5,4,3,2,1 COUNT 7 -A00000023 OK Sort completed (0.001 + 0.000 secs). +* ESEARCH (TAG "A########") UID MIN 7 MAX 1 ALL 7,6,5,4,3,2,1 COUNT 7 RELEVANCY (7 6 5 4 3 2 1) +A######## OK Sort completed (0.001 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/sort-uids-options.txt b/UnitTests/Net/Imap/Resources/dovecot/sort-uids-options.txt index 204ff5db2e..d58e31a034 100644 --- a/UnitTests/Net/Imap/Resources/dovecot/sort-uids-options.txt +++ b/UnitTests/Net/Imap/Resources/dovecot/sort-uids-options.txt @@ -1,2 +1,2 @@ -* ESEARCH (TAG "A00000059") UID MIN 1 MAX 14 ALL 1:14 COUNT 14 -A00000059 OK Sort completed (0.001 + 0.000 secs). +* ESEARCH (TAG "A########") UID MIN 1 MAX 14 ALL 1:14 COUNT 14 RELEVANCY (1 2 3 4 5 6 7 8 9 10 11 12 13 14) +A######## OK Sort completed (0.001 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/status-unittests-destination.txt b/UnitTests/Net/Imap/Resources/dovecot/status-unittests-destination.txt index f01ef512da..3a26b154af 100644 --- a/UnitTests/Net/Imap/Resources/dovecot/status-unittests-destination.txt +++ b/UnitTests/Net/Imap/Resources/dovecot/status-unittests-destination.txt @@ -1,2 +1,2 @@ * STATUS UnitTests.Destination (MESSAGES 14 RECENT 14 UIDNEXT 15 UIDVALIDITY 1436832101 UNSEEN 0 HIGHESTMODSEQ 3) -A00000029 OK Status completed (0.000 + 0.000 secs). +A######## OK Status completed (0.000 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/store-answered.txt b/UnitTests/Net/Imap/Resources/dovecot/store-answered.txt index fdb81ee9bc..e8e95962d1 100644 --- a/UnitTests/Net/Imap/Resources/dovecot/store-answered.txt +++ b/UnitTests/Net/Imap/Resources/dovecot/store-answered.txt @@ -1,4 +1,4 @@ * 1 FETCH (UID 1 MODSEQ (4)) * 2 FETCH (UID 2 MODSEQ (4)) * 3 FETCH (UID 3 MODSEQ (4)) -A00000013 OK Store completed (0.001 + 0.000 secs). +A######## OK Store completed (0.001 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/store-deleted-custom.txt b/UnitTests/Net/Imap/Resources/dovecot/store-deleted-custom.txt index 6dc31eec30..3364e8381a 100644 --- a/UnitTests/Net/Imap/Resources/dovecot/store-deleted-custom.txt +++ b/UnitTests/Net/Imap/Resources/dovecot/store-deleted-custom.txt @@ -14,4 +14,4 @@ * 12 FETCH (UID 12 MODSEQ (5)) * 13 FETCH (UID 13 MODSEQ (5)) * 14 FETCH (UID 14 MODSEQ (5)) -A00000053 OK Store completed (0.001 + 0.000 secs). +A######## OK [MODIFIED 7,9] Store completed (0.001 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/store-deleted.txt b/UnitTests/Net/Imap/Resources/dovecot/store-deleted.txt index bc19c7ed31..15fe2048e2 100644 --- a/UnitTests/Net/Imap/Resources/dovecot/store-deleted.txt +++ b/UnitTests/Net/Imap/Resources/dovecot/store-deleted.txt @@ -1,2 +1,2 @@ * 8 FETCH (UID 8 MODSEQ (5)) -A00000014 OK Store completed (0.001 + 0.000 secs). +A######## OK Store completed (0.001 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/store-seen.txt b/UnitTests/Net/Imap/Resources/dovecot/store-seen.txt index f5734f59db..670e0a45a7 100644 --- a/UnitTests/Net/Imap/Resources/dovecot/store-seen.txt +++ b/UnitTests/Net/Imap/Resources/dovecot/store-seen.txt @@ -6,4 +6,4 @@ * 6 FETCH (UID 6 MODSEQ (3)) * 7 FETCH (UID 7 MODSEQ (3)) * 8 FETCH (UID 8 MODSEQ (3)) -A00000012 OK Store completed (0.001 + 0.000 secs). +A######## OK Store completed (0.001 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/thread-orderedsubject.txt b/UnitTests/Net/Imap/Resources/dovecot/thread-orderedsubject.txt index a5088fb5cb..afd486aad7 100644 --- a/UnitTests/Net/Imap/Resources/dovecot/thread-orderedsubject.txt +++ b/UnitTests/Net/Imap/Resources/dovecot/thread-orderedsubject.txt @@ -1,2 +1,2 @@ * THREAD (1)(2)(3)(4)(5)(6)(7) -A00000017 OK Thread completed (0.010 + 0.000 secs). +A######## OK Thread completed (0.010 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/thread-references.txt b/UnitTests/Net/Imap/Resources/dovecot/thread-references.txt index c143918e2c..d126f2788f 100644 --- a/UnitTests/Net/Imap/Resources/dovecot/thread-references.txt +++ b/UnitTests/Net/Imap/Resources/dovecot/thread-references.txt @@ -1,2 +1,2 @@ * THREAD (1 (2 (3 5)(6))(4))(7) -A00000016 OK Thread completed (0.014 + 0.000 secs). +A######## OK Thread completed (0.014 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/dovecot/uid-expunge.txt b/UnitTests/Net/Imap/Resources/dovecot/uid-expunge.txt index f4a44f27e5..057c3ad60c 100644 --- a/UnitTests/Net/Imap/Resources/dovecot/uid-expunge.txt +++ b/UnitTests/Net/Imap/Resources/dovecot/uid-expunge.txt @@ -1,3 +1,3 @@ * VANISHED 8 * 7 RECENT -A00000015 OK [HIGHESTMODSEQ 6] Expunge completed (0.001 + 0.000 secs). +A######## OK [HIGHESTMODSEQ 6] Expunge completed (0.001 + 0.000 secs). diff --git a/UnitTests/Net/Imap/Resources/exchange/capability-postauth.txt b/UnitTests/Net/Imap/Resources/exchange/capability-postauth.txt new file mode 100644 index 0000000000..352d5d71d9 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/exchange/capability-postauth.txt @@ -0,0 +1,2 @@ +* CAPABILITY IMAP4 IMAP4rev1 AUTH=PLAIN AUTH=XOAUTH2 SASL-IR UIDPLUS MOVE ID UNSELECT CLIENTACCESSRULES CLIENTNETWORKPRESENCELOCATION BACKENDAUTHENTICATE CHILDREN IDLE NAMESPACE LITERAL+ +A######## OK CAPABILITY completed. diff --git a/UnitTests/Net/Imap/Resources/exchange/capability-preauth.txt b/UnitTests/Net/Imap/Resources/exchange/capability-preauth.txt new file mode 100644 index 0000000000..02486c5993 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/exchange/capability-preauth.txt @@ -0,0 +1,2 @@ +* CAPABILITY IMAP4 IMAP4rev1 AUTH=PLAIN AUTH=XOAUTH2 SASL-IR UIDPLUS ID UNSELECT CHILDREN IDLE NAMESPACE LITERAL+ +A######## OK CAPABILITY completed. diff --git a/UnitTests/Net/Imap/Resources/exchange/greeting-2003.txt b/UnitTests/Net/Imap/Resources/exchange/greeting-2003.txt new file mode 100644 index 0000000000..2bb7e0b9d6 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/exchange/greeting-2003.txt @@ -0,0 +1 @@ +* OK Microsoft Exchange Server 2003 IMAP4rev1 server version (6.5.7638.1) ready diff --git a/UnitTests/Net/Imap/Resources/exchange/greeting-2007.txt b/UnitTests/Net/Imap/Resources/exchange/greeting-2007.txt new file mode 100644 index 0000000000..ef63fa47b8 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/exchange/greeting-2007.txt @@ -0,0 +1 @@ +* OK Microsoft Exchange Server 2007 IMAP4 service is ready diff --git a/UnitTests/Net/Imap/Resources/exchange/greeting.txt b/UnitTests/Net/Imap/Resources/exchange/greeting.txt new file mode 100644 index 0000000000..3288111bac --- /dev/null +++ b/UnitTests/Net/Imap/Resources/exchange/greeting.txt @@ -0,0 +1 @@ +* OK The Microsoft Exchange IMAP4 service is ready. [TQBXAEgAUABSADIAMQBDAEEAMAAwADYAMAAuAG4AYQBtAHAAcgBkADIAMQAuAHAAcgBvAGQALgBvAHUAdABsAG8AbwBrAC4AYwBvAG0A] diff --git a/UnitTests/Net/Imap/Resources/exchange/issue115.txt b/UnitTests/Net/Imap/Resources/exchange/issue115.txt new file mode 100644 index 0000000000..e9b95e47ab --- /dev/null +++ b/UnitTests/Net/Imap/Resources/exchange/issue115.txt @@ -0,0 +1,4 @@ +[COPYUID 55 31 6] +* 1 EXPUNGE +* 0 EXISTS +A######## OK MOVE completed. diff --git a/UnitTests/Net/Imap/Resources/gmail/add-flags.txt b/UnitTests/Net/Imap/Resources/gmail/add-flags.txt index a6a3bb4367..0be0ce1f57 100644 --- a/UnitTests/Net/Imap/Resources/gmail/add-flags.txt +++ b/UnitTests/Net/Imap/Resources/gmail/add-flags.txt @@ -20,4 +20,4 @@ * 24 EXPUNGE * 30 EXPUNGE * 29 EXISTS -A00000084 OK Success +A######## OK Success diff --git a/UnitTests/Net/Imap/Resources/gmail/add-labels.txt b/UnitTests/Net/Imap/Resources/gmail/add-labels.txt new file mode 100644 index 0000000000..3c8a28f772 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/gmail/add-labels.txt @@ -0,0 +1,22 @@ +* 1 FETCH (X-GM-LABELS (\Important "Custom Label" NIL) UID 1) +* 2 FETCH (X-GM-LABELS (\Important "Custom Label" NIL) UID 2) +* 3 FETCH (X-GM-LABELS (\Important "Custom Label" NIL) UID 3) +* 5 FETCH (X-GM-LABELS (\Important "Custom Label" NIL) UID 5) +* 7 FETCH (X-GM-LABELS (\Important "Custom Label" NIL) UID 7) +* 8 FETCH (X-GM-LABELS (\Important "Custom Label" NIL) UID 8) +* 9 FETCH (X-GM-LABELS (\Important "Custom Label" NIL) UID 9) +* 11 FETCH (X-GM-LABELS (\Important "Custom Label" NIL) UID 11) +* 12 FETCH (X-GM-LABELS (\Important "Custom Label" NIL) UID 12) +* 13 FETCH (X-GM-LABELS (\Important "Custom Label" NIL) UID 13) +* 14 FETCH (X-GM-LABELS (\Important "Custom Label" NIL) UID 14) +* 26 FETCH (X-GM-LABELS (\Important "Custom Label" NIL) UID 26) +* 27 FETCH (X-GM-LABELS (\Important "Custom Label" NIL) UID 27) +* 28 FETCH (X-GM-LABELS (\Important "Custom Label" NIL) UID 28) +* 29 FETCH (X-GM-LABELS (\Important "Custom Label" NIL) UID 29) +* 31 FETCH (X-GM-LABELS (\Important "Custom Label" NIL) UID 31) +* 34 FETCH (X-GM-LABELS (\Important "Custom Label" NIL) UID 34) +* 41 FETCH (X-GM-LABELS (\Important "Custom Label" NIL) UID 41) +* 42 FETCH (X-GM-LABELS (\Important "Custom Label" NIL) UID 42) +* 43 FETCH (X-GM-LABELS (\Important "Custom Label" NIL) UID 43) +* 50 FETCH (X-GM-LABELS (\Important "Custom Label" NIL) UID 50) +A######## OK Success diff --git a/UnitTests/Net/Imap/Resources/gmail/append.1.txt b/UnitTests/Net/Imap/Resources/gmail/append.1.txt index a47660b5b0..61940d6490 100644 --- a/UnitTests/Net/Imap/Resources/gmail/append.1.txt +++ b/UnitTests/Net/Imap/Resources/gmail/append.1.txt @@ -1,2 +1,2 @@ * 1 EXISTS -A00000009 OK [APPENDUID 23 1] (Success) +A######## OK [APPENDUID 23 1] (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/append.10.txt b/UnitTests/Net/Imap/Resources/gmail/append.10.txt index 7a7376d0d2..9cd53179d3 100644 --- a/UnitTests/Net/Imap/Resources/gmail/append.10.txt +++ b/UnitTests/Net/Imap/Resources/gmail/append.10.txt @@ -1,2 +1,2 @@ * 10 EXISTS -A00000018 OK [APPENDUID 23 10] (Success) +A######## OK [APPENDUID 23 10] (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/append.11.txt b/UnitTests/Net/Imap/Resources/gmail/append.11.txt index 25ed85790b..c6b2894d3a 100644 --- a/UnitTests/Net/Imap/Resources/gmail/append.11.txt +++ b/UnitTests/Net/Imap/Resources/gmail/append.11.txt @@ -1,2 +1,2 @@ * 11 EXISTS -A00000019 OK [APPENDUID 23 11] (Success) +A######## OK [APPENDUID 23 11] (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/append.12.txt b/UnitTests/Net/Imap/Resources/gmail/append.12.txt index bfeca67cd9..aad9238ba7 100644 --- a/UnitTests/Net/Imap/Resources/gmail/append.12.txt +++ b/UnitTests/Net/Imap/Resources/gmail/append.12.txt @@ -1,2 +1,2 @@ * 12 EXISTS -A00000020 OK [APPENDUID 23 12] (Success) +A######## OK [APPENDUID 23 12] (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/append.13.txt b/UnitTests/Net/Imap/Resources/gmail/append.13.txt index 7dc56bab15..05154f1df2 100644 --- a/UnitTests/Net/Imap/Resources/gmail/append.13.txt +++ b/UnitTests/Net/Imap/Resources/gmail/append.13.txt @@ -1,2 +1,2 @@ * 13 EXISTS -A00000021 OK [APPENDUID 23 13] (Success) +A######## OK [APPENDUID 23 13] (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/append.14.txt b/UnitTests/Net/Imap/Resources/gmail/append.14.txt index a7c6e35ffe..93a9fec13c 100644 --- a/UnitTests/Net/Imap/Resources/gmail/append.14.txt +++ b/UnitTests/Net/Imap/Resources/gmail/append.14.txt @@ -1,2 +1,2 @@ * 14 EXISTS -A00000022 OK [APPENDUID 23 14] (Success) +A######## OK [APPENDUID 23 14] (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/append.15.txt b/UnitTests/Net/Imap/Resources/gmail/append.15.txt index 2a83af2491..d3c448618f 100644 --- a/UnitTests/Net/Imap/Resources/gmail/append.15.txt +++ b/UnitTests/Net/Imap/Resources/gmail/append.15.txt @@ -1,2 +1,2 @@ * 15 EXISTS -A00000023 OK [APPENDUID 23 15] (Success) +A######## OK [APPENDUID 23 15] (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/append.16.txt b/UnitTests/Net/Imap/Resources/gmail/append.16.txt index 8b4865c62e..acf64f61b6 100644 --- a/UnitTests/Net/Imap/Resources/gmail/append.16.txt +++ b/UnitTests/Net/Imap/Resources/gmail/append.16.txt @@ -1,2 +1,2 @@ * 16 EXISTS -A00000024 OK [APPENDUID 23 16] (Success) +A######## OK [APPENDUID 23 16] (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/append.17.txt b/UnitTests/Net/Imap/Resources/gmail/append.17.txt index c45a593f18..eff8117802 100644 --- a/UnitTests/Net/Imap/Resources/gmail/append.17.txt +++ b/UnitTests/Net/Imap/Resources/gmail/append.17.txt @@ -1,2 +1,2 @@ * 17 EXISTS -A00000025 OK [APPENDUID 23 17] (Success) +A######## OK [APPENDUID 23 17] (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/append.18.txt b/UnitTests/Net/Imap/Resources/gmail/append.18.txt index 8fc3e03d35..b83c1ffd10 100644 --- a/UnitTests/Net/Imap/Resources/gmail/append.18.txt +++ b/UnitTests/Net/Imap/Resources/gmail/append.18.txt @@ -1,2 +1,2 @@ * 18 EXISTS -A00000026 OK [APPENDUID 23 18] (Success) +A######## OK [APPENDUID 23 18] (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/append.19.txt b/UnitTests/Net/Imap/Resources/gmail/append.19.txt index ab1ed8cd9b..8d299e8201 100644 --- a/UnitTests/Net/Imap/Resources/gmail/append.19.txt +++ b/UnitTests/Net/Imap/Resources/gmail/append.19.txt @@ -1,2 +1,2 @@ * 19 EXISTS -A00000027 OK [APPENDUID 23 19] (Success) +A######## OK [APPENDUID 23 19] (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/append.2.txt b/UnitTests/Net/Imap/Resources/gmail/append.2.txt index fcd5af42b4..2290a0f750 100644 --- a/UnitTests/Net/Imap/Resources/gmail/append.2.txt +++ b/UnitTests/Net/Imap/Resources/gmail/append.2.txt @@ -1,2 +1,2 @@ * 2 EXISTS -A00000010 OK [APPENDUID 23 2] (Success) +A######## OK [APPENDUID 23 2] (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/append.20.txt b/UnitTests/Net/Imap/Resources/gmail/append.20.txt index c55810d33e..69a7b3f0c8 100644 --- a/UnitTests/Net/Imap/Resources/gmail/append.20.txt +++ b/UnitTests/Net/Imap/Resources/gmail/append.20.txt @@ -1,2 +1,2 @@ * 20 EXISTS -A00000028 OK [APPENDUID 23 20] (Success) +A######## OK [APPENDUID 23 20] (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/append.21.txt b/UnitTests/Net/Imap/Resources/gmail/append.21.txt index 7203f4c27e..f65a143845 100644 --- a/UnitTests/Net/Imap/Resources/gmail/append.21.txt +++ b/UnitTests/Net/Imap/Resources/gmail/append.21.txt @@ -1,2 +1,2 @@ * 21 EXISTS -A00000029 OK [APPENDUID 23 21] (Success) +A######## OK [APPENDUID 23 21] (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/append.22.txt b/UnitTests/Net/Imap/Resources/gmail/append.22.txt index fa1e0e8baa..42a5985c6e 100644 --- a/UnitTests/Net/Imap/Resources/gmail/append.22.txt +++ b/UnitTests/Net/Imap/Resources/gmail/append.22.txt @@ -1,2 +1,2 @@ * 22 EXISTS -A00000030 OK [APPENDUID 23 22] (Success) +A######## OK [APPENDUID 23 22] (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/append.23.txt b/UnitTests/Net/Imap/Resources/gmail/append.23.txt index a8a2d76a5f..ac691dd281 100644 --- a/UnitTests/Net/Imap/Resources/gmail/append.23.txt +++ b/UnitTests/Net/Imap/Resources/gmail/append.23.txt @@ -1,2 +1,2 @@ * 23 EXISTS -A00000031 OK [APPENDUID 23 23] (Success) +A######## OK [APPENDUID 23 23] (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/append.24.txt b/UnitTests/Net/Imap/Resources/gmail/append.24.txt index 5cd5648fe9..eaeba8b1d6 100644 --- a/UnitTests/Net/Imap/Resources/gmail/append.24.txt +++ b/UnitTests/Net/Imap/Resources/gmail/append.24.txt @@ -1,2 +1,2 @@ * 24 EXISTS -A00000032 OK [APPENDUID 23 24] (Success) +A######## OK [APPENDUID 23 24] (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/append.25.txt b/UnitTests/Net/Imap/Resources/gmail/append.25.txt index 3aec7f5d3d..a06e280aca 100644 --- a/UnitTests/Net/Imap/Resources/gmail/append.25.txt +++ b/UnitTests/Net/Imap/Resources/gmail/append.25.txt @@ -1,2 +1,2 @@ * 25 EXISTS -A00000033 OK [APPENDUID 23 25] (Success) +A######## OK [APPENDUID 23 25] (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/append.26.txt b/UnitTests/Net/Imap/Resources/gmail/append.26.txt index 4b77170bc3..14d20e98ef 100644 --- a/UnitTests/Net/Imap/Resources/gmail/append.26.txt +++ b/UnitTests/Net/Imap/Resources/gmail/append.26.txt @@ -1,2 +1,2 @@ * 26 EXISTS -A00000034 OK [APPENDUID 23 26] (Success) +A######## OK [APPENDUID 23 26] (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/append.27.txt b/UnitTests/Net/Imap/Resources/gmail/append.27.txt index a82ab0bde8..79ef60f2bc 100644 --- a/UnitTests/Net/Imap/Resources/gmail/append.27.txt +++ b/UnitTests/Net/Imap/Resources/gmail/append.27.txt @@ -1,2 +1,2 @@ * 27 EXISTS -A00000035 OK [APPENDUID 23 27] (Success) +A######## OK [APPENDUID 23 27] (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/append.28.txt b/UnitTests/Net/Imap/Resources/gmail/append.28.txt index 0b530086bf..927a9cdb0a 100644 --- a/UnitTests/Net/Imap/Resources/gmail/append.28.txt +++ b/UnitTests/Net/Imap/Resources/gmail/append.28.txt @@ -1,2 +1,2 @@ * 28 EXISTS -A00000036 OK [APPENDUID 23 28] (Success) +A######## OK [APPENDUID 23 28] (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/append.29.txt b/UnitTests/Net/Imap/Resources/gmail/append.29.txt index 29a96f111e..de522a8ade 100644 --- a/UnitTests/Net/Imap/Resources/gmail/append.29.txt +++ b/UnitTests/Net/Imap/Resources/gmail/append.29.txt @@ -1,2 +1,2 @@ * 29 EXISTS -A00000037 OK [APPENDUID 23 29] (Success) +A######## OK [APPENDUID 23 29] (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/append.3.txt b/UnitTests/Net/Imap/Resources/gmail/append.3.txt index 623c8f9d95..d5f0016cce 100644 --- a/UnitTests/Net/Imap/Resources/gmail/append.3.txt +++ b/UnitTests/Net/Imap/Resources/gmail/append.3.txt @@ -1,2 +1,2 @@ * 3 EXISTS -A00000011 OK [APPENDUID 23 3] (Success) +A######## OK [APPENDUID 23 3] (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/append.30.txt b/UnitTests/Net/Imap/Resources/gmail/append.30.txt index 45beb4e4f3..b4c956e538 100644 --- a/UnitTests/Net/Imap/Resources/gmail/append.30.txt +++ b/UnitTests/Net/Imap/Resources/gmail/append.30.txt @@ -1,2 +1,2 @@ * 30 EXISTS -A00000038 OK [APPENDUID 23 30] (Success) +A######## OK [APPENDUID 23 30] (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/append.31.txt b/UnitTests/Net/Imap/Resources/gmail/append.31.txt index a98ac11480..8991d16d7e 100644 --- a/UnitTests/Net/Imap/Resources/gmail/append.31.txt +++ b/UnitTests/Net/Imap/Resources/gmail/append.31.txt @@ -1,2 +1,2 @@ * 31 EXISTS -A00000039 OK [APPENDUID 23 31] (Success) +A######## OK [APPENDUID 23 31] (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/append.32.txt b/UnitTests/Net/Imap/Resources/gmail/append.32.txt index 9c77fb43b2..8ce2b6dec2 100644 --- a/UnitTests/Net/Imap/Resources/gmail/append.32.txt +++ b/UnitTests/Net/Imap/Resources/gmail/append.32.txt @@ -1,2 +1,2 @@ * 32 EXISTS -A00000040 OK [APPENDUID 23 32] (Success) +A######## OK [APPENDUID 23 32] (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/append.33.txt b/UnitTests/Net/Imap/Resources/gmail/append.33.txt index 208b8989c5..a6c8c11b97 100644 --- a/UnitTests/Net/Imap/Resources/gmail/append.33.txt +++ b/UnitTests/Net/Imap/Resources/gmail/append.33.txt @@ -1,2 +1,2 @@ * 33 EXISTS -A00000041 OK [APPENDUID 23 33] (Success) +A######## OK [APPENDUID 23 33] (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/append.34.txt b/UnitTests/Net/Imap/Resources/gmail/append.34.txt index a26af086c0..3cde61b6c7 100644 --- a/UnitTests/Net/Imap/Resources/gmail/append.34.txt +++ b/UnitTests/Net/Imap/Resources/gmail/append.34.txt @@ -1,2 +1,2 @@ * 34 EXISTS -A00000042 OK [APPENDUID 23 34] (Success) +A######## OK [APPENDUID 23 34] (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/append.35.txt b/UnitTests/Net/Imap/Resources/gmail/append.35.txt index a7a754afdb..bc81689543 100644 --- a/UnitTests/Net/Imap/Resources/gmail/append.35.txt +++ b/UnitTests/Net/Imap/Resources/gmail/append.35.txt @@ -1,2 +1,2 @@ * 35 EXISTS -A00000043 OK [APPENDUID 23 35] (Success) +A######## OK [APPENDUID 23 35] (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/append.36.txt b/UnitTests/Net/Imap/Resources/gmail/append.36.txt index 2b07bcbafa..78f3eac728 100644 --- a/UnitTests/Net/Imap/Resources/gmail/append.36.txt +++ b/UnitTests/Net/Imap/Resources/gmail/append.36.txt @@ -1,2 +1,2 @@ * 36 EXISTS -A00000044 OK [APPENDUID 23 36] (Success) +A######## OK [APPENDUID 23 36] (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/append.37.txt b/UnitTests/Net/Imap/Resources/gmail/append.37.txt index ebdc961a87..c28b57afea 100644 --- a/UnitTests/Net/Imap/Resources/gmail/append.37.txt +++ b/UnitTests/Net/Imap/Resources/gmail/append.37.txt @@ -1,2 +1,2 @@ * 37 EXISTS -A00000045 OK [APPENDUID 23 37] (Success) +A######## OK [APPENDUID 23 37] (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/append.38.txt b/UnitTests/Net/Imap/Resources/gmail/append.38.txt index 2a510218b4..02f2df1529 100644 --- a/UnitTests/Net/Imap/Resources/gmail/append.38.txt +++ b/UnitTests/Net/Imap/Resources/gmail/append.38.txt @@ -1,2 +1,2 @@ * 38 EXISTS -A00000046 OK [APPENDUID 23 38] (Success) +A######## OK [APPENDUID 23 38] (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/append.39.txt b/UnitTests/Net/Imap/Resources/gmail/append.39.txt index 0acc11c8b6..ef2c0c4819 100644 --- a/UnitTests/Net/Imap/Resources/gmail/append.39.txt +++ b/UnitTests/Net/Imap/Resources/gmail/append.39.txt @@ -1,2 +1,2 @@ * 39 EXISTS -A00000047 OK [APPENDUID 23 39] (Success) +A######## OK [APPENDUID 23 39] (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/append.4.txt b/UnitTests/Net/Imap/Resources/gmail/append.4.txt index ea3b11bed0..1f688b1e1e 100644 --- a/UnitTests/Net/Imap/Resources/gmail/append.4.txt +++ b/UnitTests/Net/Imap/Resources/gmail/append.4.txt @@ -1,2 +1,2 @@ * 4 EXISTS -A00000012 OK [APPENDUID 23 4] (Success) +A######## OK [APPENDUID 23 4] (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/append.40.txt b/UnitTests/Net/Imap/Resources/gmail/append.40.txt index bb7fc717b3..1de279855a 100644 --- a/UnitTests/Net/Imap/Resources/gmail/append.40.txt +++ b/UnitTests/Net/Imap/Resources/gmail/append.40.txt @@ -1,2 +1,2 @@ * 40 EXISTS -A00000048 OK [APPENDUID 23 40] (Success) +A######## OK [APPENDUID 23 40] (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/append.41.txt b/UnitTests/Net/Imap/Resources/gmail/append.41.txt index e05ac3f158..11f44a4c93 100644 --- a/UnitTests/Net/Imap/Resources/gmail/append.41.txt +++ b/UnitTests/Net/Imap/Resources/gmail/append.41.txt @@ -1,2 +1,2 @@ * 41 EXISTS -A00000049 OK [APPENDUID 23 41] (Success) +A######## OK [APPENDUID 23 41] (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/append.42.txt b/UnitTests/Net/Imap/Resources/gmail/append.42.txt index 7cc57b805a..ceeedc2679 100644 --- a/UnitTests/Net/Imap/Resources/gmail/append.42.txt +++ b/UnitTests/Net/Imap/Resources/gmail/append.42.txt @@ -1,2 +1,2 @@ * 42 EXISTS -A00000050 OK [APPENDUID 23 42] (Success) +A######## OK [APPENDUID 23 42] (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/append.43.txt b/UnitTests/Net/Imap/Resources/gmail/append.43.txt index b81fb2997e..fe3c46e917 100644 --- a/UnitTests/Net/Imap/Resources/gmail/append.43.txt +++ b/UnitTests/Net/Imap/Resources/gmail/append.43.txt @@ -1,2 +1,2 @@ * 43 EXISTS -A00000051 OK [APPENDUID 23 43] (Success) +A######## OK [APPENDUID 23 43] (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/append.44.txt b/UnitTests/Net/Imap/Resources/gmail/append.44.txt index 9f12907afe..b5ce3ca3e8 100644 --- a/UnitTests/Net/Imap/Resources/gmail/append.44.txt +++ b/UnitTests/Net/Imap/Resources/gmail/append.44.txt @@ -1,2 +1,2 @@ * 44 EXISTS -A00000052 OK [APPENDUID 23 44] (Success) +A######## OK [APPENDUID 23 44] (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/append.45.txt b/UnitTests/Net/Imap/Resources/gmail/append.45.txt index d5f89b8cfe..a2a7814a45 100644 --- a/UnitTests/Net/Imap/Resources/gmail/append.45.txt +++ b/UnitTests/Net/Imap/Resources/gmail/append.45.txt @@ -1,2 +1,2 @@ * 45 EXISTS -A00000053 OK [APPENDUID 23 45] (Success) +A######## OK [APPENDUID 23 45] (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/append.46.txt b/UnitTests/Net/Imap/Resources/gmail/append.46.txt index 901474f54f..2dc11ecb93 100644 --- a/UnitTests/Net/Imap/Resources/gmail/append.46.txt +++ b/UnitTests/Net/Imap/Resources/gmail/append.46.txt @@ -1,2 +1,2 @@ * 46 EXISTS -A00000054 OK [APPENDUID 23 46] (Success) +A######## OK [APPENDUID 23 46] (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/append.47.txt b/UnitTests/Net/Imap/Resources/gmail/append.47.txt index 35925f6d66..c544daea96 100644 --- a/UnitTests/Net/Imap/Resources/gmail/append.47.txt +++ b/UnitTests/Net/Imap/Resources/gmail/append.47.txt @@ -1,2 +1,2 @@ * 47 EXISTS -A00000055 OK [APPENDUID 23 47] (Success) +A######## OK [APPENDUID 23 47] (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/append.48.txt b/UnitTests/Net/Imap/Resources/gmail/append.48.txt index dd9d08b9d0..381bdfc454 100644 --- a/UnitTests/Net/Imap/Resources/gmail/append.48.txt +++ b/UnitTests/Net/Imap/Resources/gmail/append.48.txt @@ -1,2 +1,2 @@ * 48 EXISTS -A00000056 OK [APPENDUID 23 48] (Success) +A######## OK [APPENDUID 23 48] (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/append.49.txt b/UnitTests/Net/Imap/Resources/gmail/append.49.txt index 3d4181cedd..4593fe55c6 100644 --- a/UnitTests/Net/Imap/Resources/gmail/append.49.txt +++ b/UnitTests/Net/Imap/Resources/gmail/append.49.txt @@ -1,2 +1,2 @@ * 49 EXISTS -A00000057 OK [APPENDUID 23 49] (Success) +A######## OK [APPENDUID 23 49] (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/append.5.txt b/UnitTests/Net/Imap/Resources/gmail/append.5.txt index b1ee8b75e2..cce1df8068 100644 --- a/UnitTests/Net/Imap/Resources/gmail/append.5.txt +++ b/UnitTests/Net/Imap/Resources/gmail/append.5.txt @@ -1,2 +1,2 @@ * 5 EXISTS -A00000013 OK [APPENDUID 23 5] (Success) +A######## OK [APPENDUID 23 5] (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/append.50.txt b/UnitTests/Net/Imap/Resources/gmail/append.50.txt index 06a333b7d3..b75a09fce3 100644 --- a/UnitTests/Net/Imap/Resources/gmail/append.50.txt +++ b/UnitTests/Net/Imap/Resources/gmail/append.50.txt @@ -1,2 +1,2 @@ * 50 EXISTS -A00000058 OK [APPENDUID 23 50] (Success) +A######## OK [APPENDUID 23 50] (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/append.6.txt b/UnitTests/Net/Imap/Resources/gmail/append.6.txt index da232c1874..d9f9bbdb21 100644 --- a/UnitTests/Net/Imap/Resources/gmail/append.6.txt +++ b/UnitTests/Net/Imap/Resources/gmail/append.6.txt @@ -1,2 +1,2 @@ * 6 EXISTS -A00000014 OK [APPENDUID 23 6] (Success) +A######## OK [APPENDUID 23 6] (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/append.7.txt b/UnitTests/Net/Imap/Resources/gmail/append.7.txt index e0d6da790d..d168e26859 100644 --- a/UnitTests/Net/Imap/Resources/gmail/append.7.txt +++ b/UnitTests/Net/Imap/Resources/gmail/append.7.txt @@ -1,2 +1,2 @@ * 7 EXISTS -A00000015 OK [APPENDUID 23 7] (Success) +A######## OK [APPENDUID 23 7] (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/append.8.txt b/UnitTests/Net/Imap/Resources/gmail/append.8.txt index 0801f22fe9..e379499688 100644 --- a/UnitTests/Net/Imap/Resources/gmail/append.8.txt +++ b/UnitTests/Net/Imap/Resources/gmail/append.8.txt @@ -1,2 +1,2 @@ * 8 EXISTS -A00000016 OK [APPENDUID 23 8] (Success) +A######## OK [APPENDUID 23 8] (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/append.9.txt b/UnitTests/Net/Imap/Resources/gmail/append.9.txt index eff94d09cd..2e13a2ab29 100644 --- a/UnitTests/Net/Imap/Resources/gmail/append.9.txt +++ b/UnitTests/Net/Imap/Resources/gmail/append.9.txt @@ -1,2 +1,2 @@ * 9 EXISTS -A00000017 OK [APPENDUID 23 9] (Success) +A######## OK [APPENDUID 23 9] (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/authenticate+annotate.txt b/UnitTests/Net/Imap/Resources/gmail/authenticate+annotate.txt new file mode 100644 index 0000000000..649a3246e4 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/gmail/authenticate+annotate.txt @@ -0,0 +1,2 @@ +* CAPABILITY IMAP4rev1 UNSELECT IDLE NAMESPACE QUOTA ID XLIST CHILDREN X-GM-EXT-1 UIDPLUS COMPRESS=DEFLATE ENABLE MOVE CONDSTORE ESEARCH UTF8=ACCEPT LIST-EXTENDED LIST-STATUS LITERAL- APPENDLIMIT=35651584 ANNOTATE-EXPERIMENT-1 +A######## OK username authenticated (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/authenticate+create-special-use.txt b/UnitTests/Net/Imap/Resources/gmail/authenticate+create-special-use.txt new file mode 100644 index 0000000000..729a6b46f5 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/gmail/authenticate+create-special-use.txt @@ -0,0 +1,2 @@ +* CAPABILITY IMAP4rev1 UNSELECT IDLE NAMESPACE QUOTA ID XLIST CREATE-SPECIAL-USE CHILDREN X-GM-EXT-1 UIDPLUS COMPRESS=DEFLATE ENABLE MOVE CONDSTORE ESEARCH UTF8=ACCEPT LIST-EXTENDED LIST-STATUS LITERAL- APPENDLIMIT=35651584 OBJECTID +A######## OK username authenticated (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/authenticate+preview.txt b/UnitTests/Net/Imap/Resources/gmail/authenticate+preview.txt new file mode 100644 index 0000000000..5326d9062e --- /dev/null +++ b/UnitTests/Net/Imap/Resources/gmail/authenticate+preview.txt @@ -0,0 +1,2 @@ +* CAPABILITY IMAP4rev1 UNSELECT IDLE NAMESPACE QUOTA ID XLIST CHILDREN X-GM-EXT-1 UIDPLUS COMPRESS=DEFLATE ENABLE MOVE CONDSTORE ESEARCH UTF8=ACCEPT LIST-EXTENDED LIST-STATUS LITERAL- APPENDLIMIT=35651584 PREVIEW +A######## OK username authenticated (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/authenticate+savedate.txt b/UnitTests/Net/Imap/Resources/gmail/authenticate+savedate.txt new file mode 100644 index 0000000000..1a7053f8b8 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/gmail/authenticate+savedate.txt @@ -0,0 +1,2 @@ +* CAPABILITY IMAP4rev1 UNSELECT IDLE NAMESPACE QUOTA ID XLIST CHILDREN X-GM-EXT-1 UIDPLUS COMPRESS=DEFLATE ENABLE MOVE CONDSTORE ESEARCH UTF8=ACCEPT LIST-EXTENDED LIST-STATUS LITERAL- APPENDLIMIT=35651584 SAVEDATE +A######## OK username authenticated (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/authenticate+statussize+objectid.txt b/UnitTests/Net/Imap/Resources/gmail/authenticate+statussize+objectid.txt new file mode 100644 index 0000000000..4b9c6787c7 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/gmail/authenticate+statussize+objectid.txt @@ -0,0 +1,2 @@ +* CAPABILITY IMAP4rev1 UNSELECT IDLE NAMESPACE QUOTA ID XLIST CHILDREN X-GM-EXT-1 UIDPLUS COMPRESS=DEFLATE ENABLE MOVE CONDSTORE ESEARCH UTF8=ACCEPT LIST-EXTENDED LIST-STATUS LITERAL- APPENDLIMIT=35651584 STATUS=SIZE OBJECTID +A######## OK username authenticated (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/authenticate+webalert.txt b/UnitTests/Net/Imap/Resources/gmail/authenticate+webalert.txt new file mode 100644 index 0000000000..f551ff5618 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/gmail/authenticate+webalert.txt @@ -0,0 +1,2 @@ +* NO [WEBALERT https://accounts.google.com/signin/continue?sarp=1&scc=1&plt=AKgnsbsNd6RU3LIlgDfhmL9Y7ywYhtagFig_xfuSJCUHD9Eg3XqN8DKlDk3G8jmj2w5viIm5PDC3BS4SVy7iFMB6g1244cnQt1E60EdOTSEpnqDzL6FH2L-ReOAyZ3qkSXZQZs2pIfL2] Web login required. +A######## NO [ALERT] Please log in via your web browser: https://support.google.com/mail/accounts/answer/78754 (Failure) diff --git a/UnitTests/Net/Imap/Resources/gmail/authenticate-no-appendlimit-value.txt b/UnitTests/Net/Imap/Resources/gmail/authenticate-no-appendlimit-value.txt new file mode 100644 index 0000000000..12d52b17a8 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/gmail/authenticate-no-appendlimit-value.txt @@ -0,0 +1,2 @@ +* CAPABILITY IMAP4rev1 UNSELECT IDLE NAMESPACE QUOTA ID XLIST CHILDREN X-GM-EXT-1 UIDPLUS COMPRESS=DEFLATE ENABLE MOVE CONDSTORE ESEARCH UTF8=ACCEPT LIST-EXTENDED LIST-STATUS LITERAL- APPENDLIMIT STATUS=SIZE +A######## OK username authenticated (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/authenticate.txt b/UnitTests/Net/Imap/Resources/gmail/authenticate.txt index cd5e17fad5..f21a1b9ab5 100644 --- a/UnitTests/Net/Imap/Resources/gmail/authenticate.txt +++ b/UnitTests/Net/Imap/Resources/gmail/authenticate.txt @@ -1,2 +1,2 @@ * CAPABILITY IMAP4rev1 UNSELECT IDLE NAMESPACE QUOTA ID XLIST CHILDREN X-GM-EXT-1 UIDPLUS COMPRESS=DEFLATE ENABLE MOVE CONDSTORE ESEARCH UTF8=ACCEPT LIST-EXTENDED LIST-STATUS LITERAL- APPENDLIMIT=35651584 -A00000001 OK username authenticated (Success) +A######## OK username authenticated (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/capability+login.txt b/UnitTests/Net/Imap/Resources/gmail/capability+login.txt new file mode 100644 index 0000000000..102615a610 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/gmail/capability+login.txt @@ -0,0 +1,2 @@ +* CAPABILITY IMAP4rev1 UNSELECT IDLE NAMESPACE QUOTA ID XLIST CHILDREN X-GM-EXT-1 XYZZY SASL-IR AUTH=XOAUTH2 AUTH=LOGIN AUTH=PLAIN AUTH=PLAIN-CLIENTTOKEN AUTH=OAUTHBEARER AUTH=XOAUTH +A######## OK Thats all she wrote! n55mb83651539qtb diff --git a/UnitTests/Net/Imap/Resources/gmail/capability+logindisabled.txt b/UnitTests/Net/Imap/Resources/gmail/capability+logindisabled.txt new file mode 100644 index 0000000000..057d0134d4 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/gmail/capability+logindisabled.txt @@ -0,0 +1,2 @@ +* CAPABILITY IMAP4rev1 UNSELECT IDLE NAMESPACE QUOTA ID XLIST CHILDREN X-GM-EXT-1 XYZZY SASL-IR LOGINDISABLED AUTH=XOAUTH2 AUTH=PLAIN AUTH=PLAIN-CLIENTTOKEN AUTH=OAUTHBEARER AUTH=XOAUTH +A######## OK Thats all she wrote! n55mb83651539qtb diff --git a/UnitTests/Net/Imap/Resources/gmail/capability.txt b/UnitTests/Net/Imap/Resources/gmail/capability.txt index 801a0a700a..caa41e6ced 100644 --- a/UnitTests/Net/Imap/Resources/gmail/capability.txt +++ b/UnitTests/Net/Imap/Resources/gmail/capability.txt @@ -1,2 +1,2 @@ * CAPABILITY IMAP4rev1 UNSELECT IDLE NAMESPACE QUOTA ID XLIST CHILDREN X-GM-EXT-1 XYZZY SASL-IR AUTH=XOAUTH2 AUTH=PLAIN AUTH=PLAIN-CLIENTTOKEN AUTH=OAUTHBEARER AUTH=XOAUTH -A00000000 OK Thats all she wrote! n55mb83651539qtb +A######## OK Thats all she wrote! n55mb83651539qtb diff --git a/UnitTests/Net/Imap/Resources/gmail/count-explicit.noop.txt b/UnitTests/Net/Imap/Resources/gmail/count-explicit.noop.txt new file mode 100644 index 0000000000..4faf6eea0d --- /dev/null +++ b/UnitTests/Net/Imap/Resources/gmail/count-explicit.noop.txt @@ -0,0 +1,3 @@ +* 1 EXPUNGE +* 1 EXISTS +A######## OK NOOP completed diff --git a/UnitTests/Net/Imap/Resources/gmail/count-implicit.noop.txt b/UnitTests/Net/Imap/Resources/gmail/count-implicit.noop.txt new file mode 100644 index 0000000000..976b7385c8 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/gmail/count-implicit.noop.txt @@ -0,0 +1,2 @@ +* 1 EXPUNGE +A######## OK NOOP completed diff --git a/UnitTests/Net/Imap/Resources/gmail/count.examine.txt b/UnitTests/Net/Imap/Resources/gmail/count.examine.txt index d0fb765b53..8a9308ac59 100644 --- a/UnitTests/Net/Imap/Resources/gmail/count.examine.txt +++ b/UnitTests/Net/Imap/Resources/gmail/count.examine.txt @@ -5,4 +5,4 @@ * 0 RECENT * OK [UIDNEXT 2] Predicted next UID. * OK [HIGHESTMODSEQ 29225] -A00000005 OK [READ-WRITE] Inbox selected. (Success) +A######## OK [READ-WRITE] Inbox selected. (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/count.noop.txt b/UnitTests/Net/Imap/Resources/gmail/count.noop.txt deleted file mode 100644 index bce04d1fba..0000000000 --- a/UnitTests/Net/Imap/Resources/gmail/count.noop.txt +++ /dev/null @@ -1,3 +0,0 @@ -* 1 EXPUNGE -* 1 EXISTS -A00000006 OK NOOP completed diff --git a/UnitTests/Net/Imap/Resources/gmail/create-mailboxid.txt b/UnitTests/Net/Imap/Resources/gmail/create-mailboxid.txt new file mode 100644 index 0000000000..1f89d2823f --- /dev/null +++ b/UnitTests/Net/Imap/Resources/gmail/create-mailboxid.txt @@ -0,0 +1 @@ +A######## OK [MAILBOXID (25dcfa84-fd65-41c3-abc3-633c8f10923f)] Thats all she wrote! n55mb83651539qtb diff --git a/UnitTests/Net/Imap/Resources/gmail/examine-inbox.txt b/UnitTests/Net/Imap/Resources/gmail/examine-inbox.txt index d987823d3a..c942167926 100644 --- a/UnitTests/Net/Imap/Resources/gmail/examine-inbox.txt +++ b/UnitTests/Net/Imap/Resources/gmail/examine-inbox.txt @@ -5,4 +5,4 @@ * 0 RECENT * OK [UIDNEXT 280] Predicted next UID. * OK [HIGHESTMODSEQ 35312] -A00000006 OK [READ-ONLY] INBOX selected. (Success) +A######## OK [READ-ONLY] INBOX selected. (Success) diff --git a/UnitTests/Net/Imap/Resources/gmail/expunge-during-fetch.txt b/UnitTests/Net/Imap/Resources/gmail/expunge-during-fetch.txt new file mode 100644 index 0000000000..03544bafcb --- /dev/null +++ b/UnitTests/Net/Imap/Resources/gmail/expunge-during-fetch.txt @@ -0,0 +1,9 @@ +* 1 FETCH (UID 1 INTERNALDATE "19-Jul-2019 06:00:17 +0000" ENVELOPE ("Fri, 19 Jul 2019 06:00:17 +0000" "subject 1" (("From" NIL "from" "example.com")) NIL NIL ((NIL NIL "to" "example.com")) NIL NIL "in-reply-to@localhost.localdomain" "")) +* 2 FETCH (UID 2 INTERNALDATE "19-Jul-2019 06:00:17 +0000" ENVELOPE ("Fri, 19 Jul 2019 06:00:17 +0000" "subject 2" (("From" NIL "from" "example.com")) NIL NIL ((NIL NIL "to" "example.com")) NIL NIL "in-reply-to@localhost.localdomain" "")) +* 3 FETCH (UID 3 INTERNALDATE "19-Jul-2019 06:00:17 +0000" ENVELOPE ("Fri, 19 Jul 2019 06:00:17 +0000" "subject 3" (("From" NIL "from" "example.com")) NIL NIL ((NIL NIL "to" "example.com")) NIL NIL "in-reply-to@localhost.localdomain" "")) +* 4 FETCH (UID 4 INTERNALDATE "19-Jul-2019 06:00:17 +0000" ENVELOPE ("Fri, 19 Jul 2019 06:00:17 +0000" "subject 4" (("From" NIL "from" "example.com")) NIL NIL ((NIL NIL "to" "example.com")) NIL NIL "in-reply-to@localhost.localdomain" "")) +* 5 FETCH (UID 5 INTERNALDATE "19-Jul-2019 06:00:17 +0000" ENVELOPE ("Fri, 19 Jul 2019 06:00:17 +0000" "subject 5" (("From" NIL "from" "example.com")) NIL NIL ((NIL NIL "to" "example.com")) NIL NIL "in-reply-to@localhost.localdomain" "")) +* 2 EXPUNGE +* 5 EXPUNGE +* 4 EXISTS +A######## OK FETCH completed. diff --git a/UnitTests/Net/Imap/Resources/gmail/expunge.txt b/UnitTests/Net/Imap/Resources/gmail/expunge.txt new file mode 100644 index 0000000000..fa369bee88 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/gmail/expunge.txt @@ -0,0 +1,5 @@ +* 1 EXPUNGE +* 1 EXPUNGE +* 1 EXPUNGE +* 18 EXISTS +A######## OK Success diff --git a/UnitTests/Net/Imap/Resources/gmail/fetch-all-headers.txt b/UnitTests/Net/Imap/Resources/gmail/fetch-all-headers.txt new file mode 100644 index 0000000000..0b31715e60 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/gmail/fetch-all-headers.txt @@ -0,0 +1,55 @@ +* 1 FETCH (UID 1 FLAGS (\Seen) BODY[HEADER] {275} +Date: Thu, 03 Mar 2016 12:25:01 -0500 +Subject: text/plain message +From: "Example From" +Reply-To: "Example Reply-To" +To: "Example To" +Message-Id: <2T1YDYWYKXT4.7B2PY7UJXN1U3@Jeffrey-Stedfasts-2013-iMac.local> + +) +* 2 FETCH (UID 2 FLAGS (\Seen) BODY[HEADER] {275} +Date: Thu, 03 Mar 2016 12:25:01 -0500 +Subject: text/plain message +From: "Example From" +Reply-To: "Example Reply-To" +To: "Example To" +Message-Id: <2T1YDYWYKXT4.7B2PY7UJXN1U3@Jeffrey-Stedfasts-2013-iMac.local> + +) +* 3 FETCH (UID 3 FLAGS (\Seen) BODY[HEADER] {275} +Date: Thu, 03 Mar 2016 12:25:01 -0500 +Subject: text/plain message +From: "Example From" +Reply-To: "Example Reply-To" +To: "Example To" +Message-Id: <2T1YDYWYKXT4.7B2PY7UJXN1U3@Jeffrey-Stedfasts-2013-iMac.local> + +) +* 4 FETCH (UID 4 FLAGS (\Seen) BODY[HEADER] {275} +Date: Thu, 03 Mar 2016 12:25:01 -0500 +Subject: text/plain message +From: "Example From" +Reply-To: "Example Reply-To" +To: "Example To" +Message-Id: <2T1YDYWYKXT4.7B2PY7UJXN1U3@Jeffrey-Stedfasts-2013-iMac.local> + +) +* 5 FETCH (UID 5 FLAGS (\Seen) BODY[HEADER] {275} +Date: Thu, 03 Mar 2016 12:25:01 -0500 +Subject: text/plain message +From: "Example From" +Reply-To: "Example Reply-To" +To: "Example To" +Message-Id: <2T1YDYWYKXT4.7B2PY7UJXN1U3@Jeffrey-Stedfasts-2013-iMac.local> + +) +* 6 FETCH (UID 6 FLAGS (\Seen) BODY[HEADER] {275} +Date: Thu, 03 Mar 2016 12:25:01 -0500 +Subject: text/plain message +From: "Example From" +Reply-To: "Example Reply-To" +To: "Example To" +Message-Id: <2T1YDYWYKXT4.7B2PY7UJXN1U3@Jeffrey-Stedfasts-2013-iMac.local> + +) +A######## OK Success diff --git a/UnitTests/Net/Imap/Resources/gmail/fetch-invalid-headers.txt b/UnitTests/Net/Imap/Resources/gmail/fetch-invalid-headers.txt new file mode 100644 index 0000000000..d1b79e4f35 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/gmail/fetch-invalid-headers.txt @@ -0,0 +1,13 @@ +* 1 FETCH (UID 1 FLAGS (\Seen) BODY[HEADER] {27} +!@&^*$(*&E WOIFDUJS Fu87#$*) +* 2 FETCH (UID 2 FLAGS (\Seen) BODY[HEADER] {27} +!@&^*$(*&E WOIFDUJS Fu87#$*) +* 3 FETCH (UID 3 FLAGS (\Seen) BODY[HEADER] {27} +!@&^*$(*&E WOIFDUJS Fu87#$*) +* 4 FETCH (UID 4 FLAGS (\Seen) BODY[HEADER] {27} +!@&^*$(*&E WOIFDUJS Fu87#$*) +* 5 FETCH (UID 5 FLAGS (\Seen) BODY[HEADER] {27} +!@&^*$(*&E WOIFDUJS Fu87#$*) +* 6 FETCH (UID 6 FLAGS (\Seen) BODY[HEADER] {27} +!@&^*$(*&E WOIFDUJS Fu87#$*) +A######## OK Success diff --git a/UnitTests/Net/Imap/Resources/gmail/fetch-korean-previewtext-bodystructure.txt b/UnitTests/Net/Imap/Resources/gmail/fetch-korean-previewtext-bodystructure.txt new file mode 100644 index 0000000000..2d62d07680 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/gmail/fetch-korean-previewtext-bodystructure.txt @@ -0,0 +1,2 @@ +* 1 FETCH (UID 1 BODYSTRUCTURE ("TEXT" "PLAIN" ("CHARSET" "ks_c_5601-1987") NIL NIL "BASE64" 1896 25 NIL NIL NIL)) +A######## OK Success diff --git a/UnitTests/Net/Imap/Resources/gmail/fetch-korean-previewtext-peek-text-only.txt b/UnitTests/Net/Imap/Resources/gmail/fetch-korean-previewtext-peek-text-only.txt new file mode 100644 index 0000000000..4815bd2e46 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/gmail/fetch-korean-previewtext-peek-text-only.txt @@ -0,0 +1,9 @@ +* 1 FETCH (UID 1 BODY[TEXT]<0> {512} +vK2x4iAyNTCz4rDmILDtutAgvcO067ChIL3DwNu1x7jpvK0gsO260MDMtvOw7SC60riutMIgsMW0 +68fRILmrtP3AzCCwx7yztcggsM3AuiC6uLTZIMH9vuDA+8DOILPzvvew+iDDtrHiILHivPrAxyC1 +tcDUv6EgyPvA1L7uILCtt8LH0SDA/LvnIL+kuK7GrsDHIMPix/bAuyCzqsW4s8C0z7TZLiDAz7q7 +wLogw7aw+iCx4sW4ILmwx7DAxyCw+LHewLsgyK66uMfPseIgwKfH2CCzssfRwMcgv6y+yCDB9rno +IMH9tNyw+iDB/cHfwPvAziDBosPLwLsgufrAzLjpvK0gwd+xub+hILvnvcXAuyDGxLDfx8+46byt +ILTrt/ogurvF5L/NwMcgwaLDy8DMIMH1sKHH373AtM+02SgyMzgsIDI0MywgMjQ3KS4gNLy8seIg +tb++yCDB9rzTtcggx9G53bW1wMcgx9GxuSC8vLfCsPrA) +A######## OK Success diff --git a/UnitTests/Net/Imap/Resources/gmail/fetch-nil-bodystructure.txt b/UnitTests/Net/Imap/Resources/gmail/fetch-nil-bodystructure.txt new file mode 100644 index 0000000000..fa647c4167 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/gmail/fetch-nil-bodystructure.txt @@ -0,0 +1,2 @@ +* 1 FETCH (UID 1 BODYSTRUCTURE (("TEXT" "PLAIN" ("CHARSET" "utf-8") NIL NIL "7BIT" 5235 112 NIL NIL NIL)("TEXT" "HTML" ("CHARSET" "utf-8") NIL NIL "7BIT" 60479 873 NIL NIL NIL) "ALTERNATIVE")) +A######## OK Success diff --git a/UnitTests/Net/Imap/Resources/gmail/fetch-nil.txt b/UnitTests/Net/Imap/Resources/gmail/fetch-nil.txt new file mode 100644 index 0000000000..b31169e69d --- /dev/null +++ b/UnitTests/Net/Imap/Resources/gmail/fetch-nil.txt @@ -0,0 +1,2 @@ +* 3 FETCH (UID 1 BODY[1.TEXT]<0> NIL) +A######## OK Success diff --git a/UnitTests/Net/Imap/Resources/gmail/fetch-objectid.txt b/UnitTests/Net/Imap/Resources/gmail/fetch-objectid.txt new file mode 100644 index 0000000000..5f01c00708 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/gmail/fetch-objectid.txt @@ -0,0 +1,5 @@ +* 1 FETCH (UID 1 EMAILID (M6d99ac3275bb4e) THREADID (T64b478a75b7ea9)) +* 2 FETCH (UID 2 EMAILID (M288836c4c7a762) THREADID (T64b478a75b7ea9)) +* 3 FETCH (UID 3 EMAILID (M5fdc09b49ea703) THREADID (T11863d02dd95b5)) +* 4 FETCH (UID 4 EMAILID (M4fdc09b49ea629) THREADID NIL) +A######## OK Success diff --git a/UnitTests/Net/Imap/Resources/gmail/fetch-preview.txt b/UnitTests/Net/Imap/Resources/gmail/fetch-preview.txt new file mode 100644 index 0000000000..484fe5a476 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/gmail/fetch-preview.txt @@ -0,0 +1,7 @@ +* 1 FETCH (UID 1 RFC822.SIZE 507 MODSEQ (41847) INTERNALDATE "03-Mar-2016 17:25:01 +0000" FLAGS (\Seen) ENVELOPE ("Thu, 03 Mar 2016 12:25:01 -0500" "text/plain message" (("Example From" NIL "from" "example.com")) (("Example From" NIL "from" "example.com")) (("Example Reply-To" NIL "reply-to" "example.com")) (("Example To" NIL "to" "example.com")) (("Example Recipient #1" NIL "recipient1" "example.com")("Example Recipient #2" NIL "recipient2" "example.com")("Example Recipient #3" NIL "recipient3" "example.com")) NIL NIL "<2T1YDYWYKXT4.7B2PY7UJXN1U3@Jeffrey-Stedfasts-2013-iMac.local>") PREVIEW "Planet Fitness https://view.email.planetfitness.com/?qs=9a098a031cabde68c0a4260051cd6fe473a2e997a53678ff26b4b199a711a9d2ad0536530d6f837c246b09f644d42016ecfb298f930b7af058e9e454b34f3d818ceb3052ae317b1ac4594aab28a2d788 View web ver") +* 2 FETCH (UID 2 RFC822.SIZE 506 MODSEQ (41847) INTERNALDATE "03-Mar-2016 17:48:38 +0000" FLAGS (\Seen) ENVELOPE ("Thu, 03 Mar 2016 12:46:38 -0500" "text/html message" (("Example From" NIL "from" "example.com")) (("Example From" NIL "from" "example.com")) (("Example Reply-To" NIL "reply-to" "example.com")) (("Example To" NIL "to" "example.com")) (("Example Recipient #1" NIL "recipient1" "example.com")("Example Recipient #2" NIL "recipient2" "example.com")("Example Recipient #3" NIL "recipient3" "example.com")) NIL NIL "") PREVIEW "Don't miss our celebrity guest Monday evening") +* 3 FETCH (UID 3 RFC822.SIZE 507 MODSEQ (41847) INTERNALDATE "03-Mar-2016 17:49:29 +0000" FLAGS (\Seen) ENVELOPE ("Thu, 03 Mar 2016 12:47:29 -0500" "multipart/alternative message" (("Example From" NIL "from" "example.com")) (("Example From" NIL "from" "example.com")) (("Example Reply-To" NIL "reply-to" "example.com")) (("Example To" NIL "to" "example.com")) (("Example Recipient #1" NIL "recipient1" "example.com")("Example Recipient #2" NIL "recipient2" "example.com")("Example Recipient #3" NIL "recipient3" "example.com")) NIL NIL "<014Y2P3ZKXT4.TMD6XILA8I5N2@Jeffrey-Stedfasts-2013-iMac.local>") PREVIEW "Planet Fitness https://view.email.planetfitness.com/?qs=9a098a031cabde68c0a4260051cd6fe473a2e997a53678ff26b4b199a711a9d2ad0536530d6f837c246b09f644d42016ecfb298f930b7af058e9e454b34f3d818ceb3052ae317b1ac4594aab28a2d788 View web ver") +* 4 FETCH (UID 4 RFC822.SIZE 507 MODSEQ (41847) INTERNALDATE "03-Mar-2016 17:25:01 +0000" FLAGS (\Seen) ENVELOPE ("Thu, 03 Mar 2016 12:50:01 -0500" "text/plain message" (("Example From" NIL "from" "example.com")) (("Example From" NIL "from" "example.com")) (("Example Reply-To" NIL "reply-to" "example.com")) (("Example To" NIL "to" "example.com")) (("Example Recipient #1" NIL "recipient1" "example.com")("Example Recipient #2" NIL "recipient2" "example.com")("Example Recipient #3" NIL "recipient3" "example.com")) NIL NIL "<2T1YDYWYKXT4.7B2PY7UJXN1U3@Jeffrey-Stedfasts-2013-iMac.local>") PREVIEW "Planet Fitness https://view.email.planetfitness.com/?qs=9a098a031cabde68c0a4260051cd6fe473a2e997a53678ff26b4b199a711a9d2ad0536530d6f837c246b09f644d42016ecfb298f930b7af058e9e454b34f3d818ceb3052ae317b1ac4594aab28a2d788 View web ver") +* 5 FETCH (UID 5 RFC822.SIZE 506 MODSEQ (41847) INTERNALDATE "03-Mar-2016 17:48:38 +0000" FLAGS (\Seen) ENVELOPE ("Thu, 03 Mar 2016 12:51:38 -0500" "text/html message" (("Example From" NIL "from" "example.com")) (("Example From" NIL "from" "example.com")) (("Example Reply-To" NIL "reply-to" "example.com")) (("Example To" NIL "to" "example.com")) (("Example Recipient #1" NIL "recipient1" "example.com")("Example Recipient #2" NIL "recipient2" "example.com")("Example Recipient #3" NIL "recipient3" "example.com")) NIL NIL "") PREVIEW "Don't miss our celebrity guest Monday evening") +* 6 FETCH (UID 6 RFC822.SIZE 507 MODSEQ (41847) INTERNALDATE "03-Mar-2016 17:49:29 +0000" FLAGS (\Seen) ENVELOPE ("Thu, 03 Mar 2016 12:52:29 -0500" "multipart/alternative message" (("Example From" NIL "from" "example.com")) (("Example From" NIL "from" "example.com")) (("Example Reply-To" NIL "reply-to" "example.com")) (("Example To" NIL "to" "example.com")) (("Example Recipient #1" NIL "recipient1" "example.com")("Example Recipient #2" NIL "recipient2" "example.com")("Example Recipient #3" NIL "recipient3" "example.com")) NIL NIL "<014Y2P3ZKXT4.TMD6XILA8I5N2@Jeffrey-Stedfasts-2013-iMac.local>") PREVIEW "Planet Fitness https://view.email.planetfitness.com/?qs=9a098a031cabde68c0a4260051cd6fe473a2e997a53678ff26b4b199a711a9d2ad0536530d6f837c246b09f644d42016ecfb298f930b7af058e9e454b34f3d818ceb3052ae317b1ac4594aab28a2d788 View web ver") +A######## OK Success diff --git a/UnitTests/Net/Imap/Resources/gmail/fetch-previewtext-bodystructure.txt b/UnitTests/Net/Imap/Resources/gmail/fetch-previewtext-bodystructure.txt new file mode 100644 index 0000000000..a55ca8d558 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/gmail/fetch-previewtext-bodystructure.txt @@ -0,0 +1,8 @@ +* 1 FETCH (UID 1 RFC822.SIZE 507 MODSEQ (41847) INTERNALDATE "03-Mar-2016 17:25:01 +0000" FLAGS (\Seen) ENVELOPE ("Thu, 03 Mar 2016 12:25:01 -0500" "text/plain message" (("Example From" NIL "from" "example.com")) (("Example From" NIL "from" "example.com")) (("Example Reply-To" NIL "reply-to" "example.com")) (("Example To" NIL "to" "example.com")) (("Example Recipient #1" NIL "recipient1" "example.com")("Example Recipient #2" NIL "recipient2" "example.com")("Example Recipient #3" NIL "recipient3" "example.com")) NIL NIL "<2T1YDYWYKXT4.7B2PY7UJXN1U3@Jeffrey-Stedfasts-2013-iMac.local>") BODYSTRUCTURE ("TEXT" "PLAIN" ("CHARSET" "utf-8") NIL NIL "7BIT" 5235 112 NIL NIL NIL)) +* 2 FETCH (UID 2 RFC822.SIZE 506 MODSEQ (41847) INTERNALDATE "03-Mar-2016 17:48:38 +0000" FLAGS (\Seen) ENVELOPE ("Thu, 03 Mar 2016 12:46:38 -0500" "text/html message" (("Example From" NIL "from" "example.com")) (("Example From" NIL "from" "example.com")) (("Example Reply-To" NIL "reply-to" "example.com")) (("Example To" NIL "to" "example.com")) (("Example Recipient #1" NIL "recipient1" "example.com")("Example Recipient #2" NIL "recipient2" "example.com")("Example Recipient #3" NIL "recipient3" "example.com")) NIL NIL "") BODYSTRUCTURE ("TEXT" "HTML" ("CHARSET" "utf-8") NIL NIL "7BIT" 60479 873 NIL NIL NIL)) +* 3 FETCH (UID 3 RFC822.SIZE 507 MODSEQ (41847) INTERNALDATE "03-Mar-2016 17:49:29 +0000" FLAGS (\Seen) ENVELOPE ("Thu, 03 Mar 2016 12:47:29 -0500" "multipart/alternative message" (("Example From" NIL "from" "example.com")) (("Example From" NIL "from" "example.com")) (("Example Reply-To" NIL "reply-to" "example.com")) (("Example To" NIL "to" "example.com")) (("Example Recipient #1" NIL "recipient1" "example.com")("Example Recipient #2" NIL "recipient2" "example.com")("Example Recipient #3" NIL "recipient3" "example.com")) NIL NIL "<014Y2P3ZKXT4.TMD6XILA8I5N2@Jeffrey-Stedfasts-2013-iMac.local>") BODYSTRUCTURE (("TEXT" "PLAIN" ("CHARSET" "utf-8") NIL NIL "7BIT" 5235 112 NIL NIL NIL)("TEXT" "HTML" ("CHARSET" "utf-8") NIL NIL "7BIT" 60479 873 NIL NIL NIL) "ALTERNATIVE")) +* 4 FETCH (UID 4 RFC822.SIZE 507 MODSEQ (41847) INTERNALDATE "03-Mar-2016 17:25:01 +0000" FLAGS (\Seen) ENVELOPE ("Thu, 03 Mar 2016 12:50:01 -0500" "text/plain message" (("Example From" NIL "from" "example.com")) (("Example From" NIL "from" "example.com")) (("Example Reply-To" NIL "reply-to" "example.com")) (("Example To" NIL "to" "example.com")) (("Example Recipient #1" NIL "recipient1" "example.com")("Example Recipient #2" NIL "recipient2" "example.com")("Example Recipient #3" NIL "recipient3" "example.com")) NIL NIL "<2T1YDYWYKXT4.7B2PY7UJXN1U3@Jeffrey-Stedfasts-2013-iMac.local>") BODYSTRUCTURE ("TEXT" "PLAIN" ("CHARSET" "utf-8") NIL NIL "7BIT" 5235 112 NIL NIL NIL)) +* 5 FETCH (UID 5 RFC822.SIZE 506 MODSEQ (41847) INTERNALDATE "03-Mar-2016 17:48:38 +0000" FLAGS (\Seen) ENVELOPE ("Thu, 03 Mar 2016 12:51:38 -0500" "text/html message" (("Example From" NIL "from" "example.com")) (("Example From" NIL "from" "example.com")) (("Example Reply-To" NIL "reply-to" "example.com")) (("Example To" NIL "to" "example.com")) (("Example Recipient #1" NIL "recipient1" "example.com")("Example Recipient #2" NIL "recipient2" "example.com")("Example Recipient #3" NIL "recipient3" "example.com")) NIL NIL "") BODYSTRUCTURE ("TEXT" "HTML" ("CHARSET" "utf-8") NIL NIL "7BIT" 60479 873 NIL NIL NIL)) +* 6 FETCH (UID 6 RFC822.SIZE 507 MODSEQ (41847) INTERNALDATE "03-Mar-2016 17:49:29 +0000" FLAGS (\Seen) ENVELOPE ("Thu, 03 Mar 2016 12:52:29 -0500" "multipart/alternative message" (("Example From" NIL "from" "example.com")) (("Example From" NIL "from" "example.com")) (("Example Reply-To" NIL "reply-to" "example.com")) (("Example To" NIL "to" "example.com")) (("Example Recipient #1" NIL "recipient1" "example.com")("Example Recipient #2" NIL "recipient2" "example.com")("Example Recipient #3" NIL "recipient3" "example.com")) NIL NIL "<014Y2P3ZKXT4.TMD6XILA8I5N2@Jeffrey-Stedfasts-2013-iMac.local>") BODYSTRUCTURE (("TEXT" "PLAIN" ("CHARSET" "utf-8") NIL NIL "7BIT" 5235 112 NIL NIL NIL)("TEXT" "HTML" ("CHARSET" "utf-8") NIL NIL "7BIT" 60479 873 NIL NIL NIL) "ALTERNATIVE")) +* 7 FETCH (UID 7 RFC822.SIZE 1024 MODSEQ (41847) INTERNALDATE "03-Mar-2016 17:25:01 +0000" FLAGS (\Seen) ENVELOPE ("Thu, 03 Mar 2016 12:25:01 -0500" "audio/wav message" (("Example From" NIL "from" "example.com")) (("Example From" NIL "from" "example.com")) (("Example Reply-To" NIL "reply-to" "example.com")) (("Example To" NIL "to" "example.com")) (("Example Recipient #1" NIL "recipient1" "example.com")("Example Recipient #2" NIL "recipient2" "example.com")("Example Recipient #3" NIL "recipient3" "example.com")) NIL NIL "<2T1YDYWYKXT4.7B2PY7UJXN1U3@Jeffrey-Stedfasts-2013-iMac.local>") BODYSTRUCTURE ("AUDIO" "WAV" NIL NIL NIL "BASE64" 5235 NIL NIL NIL)) +A######## OK Success diff --git a/UnitTests/Net/Imap/Resources/gmail/fetch-previewtext-peek-html-only.txt b/UnitTests/Net/Imap/Resources/gmail/fetch-previewtext-peek-html-only.txt new file mode 100644 index 0000000000..6e418ad8ce --- /dev/null +++ b/UnitTests/Net/Imap/Resources/gmail/fetch-previewtext-peek-html-only.txt @@ -0,0 +1,1003 @@ +* 2 FETCH (UID 2 BODY[TEXT]<0> {16384} + + + + + Planet Fitness + + + + + + + + + + + + + + + + +
Don’t miss our celebrity guest Monday evening
+ + + + +
+ + +
+ + + +